Python Module Bank - 单文件模块仓库
把 Python 模块打包进一个文件(.mbank),运行时直接从包里导入,支持 AES 字节码加密
和 PyArmor 代码混淆 双重保护。
存储层是 sqlite_vfs:
.mbank 就是一个 标准 svfs 包——一个用 SQLite 实现的虚拟文件系统,模块、源码、
PyArmor 运行时都以文件形式躺在里面。
🌟 特性
- 单文件分发 - 所有模块打包到单个
.mbank;它同时是标准.svfs包,svfs/vfsverify/vfsunpack/svfs publish这些工具都能直接处理它 - AES 字节码加密 - 编译后的字节码使用 Fernet (AES-128-CBC) 加密存储
- PyArmor 代码混淆 - 支持使用 PyArmor 对 Python 源代码进行混淆保护
- 双层保护 - 可同时使用 PyArmor 混淆 + AES 字节码加密,提供双重安全保障
- 动态导入 - 运行时直接从包加载模块,无缝集成 Python 导入系统
- 完整包支持 - 支持包结构和子模块导入
- 内容去重与压缩 - 内容按 sha256 寻址(同一份运行时/文件只存一次),写入时 zlib 压缩(压不动的内容原样存,不浪费)
- 可校验 - 快照带摘要,
vfsverify能验证包内容是否被改过 - CLI 工具 - 提供完整的命令行接口
📦 安装
从源码安装
git clone http://124.71.68.6:3000/chakcy/module_bank.git
cd module-bank
pip install -e .
使用 uv(推荐)
git clone http://124.71.68.6:3000/chakcy/module_bank.git
cd module-bank
uv sync
可选依赖
- PyArmor (强烈推荐):
uv add pyarmor或pip install pyarmor- 为模块提供 Python 源代码级别的混淆保护
- 商业版可去除试用版水印
存储层依赖
sqlite-vfs 是必装依赖,在 pyproject.toml 里以 git 依赖声明。发布到 PyPI 之前,
v0.4.0 那个 tag 必须先推到 Gitea,否则 uv sync / pip install 装不上(那个
仓库是私有的话,使用者还要能访问它,比如配好凭据助手或 .netrc)。
本地开发期间还没推 tag 时,把 pyproject.toml 里注释掉的 [tool.uv.sources]
覆盖打开(指向本机 sqlite_vfs 目录),或者直接
uv pip install -e <sqlite_vfs 路径>。
🚀 快速开始
1. 创建示例模块
# my_package/__init__.py
def hello():
print("Hello from my_package!")
return "success"
VERSION = "1.0.0"
# my_package/utils.py
def add(a, b):
return a + b
2. 打包模块到数据库
使用 AES 加密(推荐)
# 生成密钥并打包
mbank pack my_package --db my_package.mbank --key "$(python -c "from module_bank.encryption import Encryption; print(Encryption.generate_key())")"
使用 PyArmor 混淆
# 使用 PyArmor 保护源代码
mbank pack my_package --db my_package.mbank --pyarmor
使用双层保护
# PyArmor 混淆 + AES 加密
mbank pack my_package --db my_package.mbank --pyarmor --key "your-encryption-key"
3. 从数据库导入
# import_example.py
from module_bank.python_to_sqlite import PythonToSQLite
packer = PythonToSQLite("my_package.mbank")
finder = packer.install_importer(key="your-encryption-key") # 如果加密了需要提供密钥
from my_package import utils
from my_package import hello
print(hello())
print(utils.add(3, 4))
🛡️ 安全保护机制
第一层:PyArmor 代码混淆
PyArmor 对 Python 源代码进行混淆,将代码转换为难以阅读和逆向的形式:
- 混淆后的代码依赖运行时二进制文件(
.pyd/.so),无法直接运行 - 支持混淆级别控制(代码混淆、模块混淆)
- 运行时文件会自动存储在包中(
runtime/),加载时自动部署到临时目录 - 整个包只跑一次 PyArmor,因此整包只存一份运行时:PyArmor 每次调用都会 生成一份自己的运行时(同名、内含那一次的新密钥、约 639 KB),逐模块调一次就会 让包体积按模块数线性膨胀(实测 6 个模块 2.3 倍、30 个模块约 6 倍)
# 高级混淆选项
mbank pack my_module.py --name my_module --db modules.mbank \
--pyarmor \
--pyarmor-obf-code 1 \
--pyarmor-obf-mod 1
第二层:AES 字节码加密
编译后的字节码使用 Fernet (AES-128-CBC) 加密:
# 生成密钥
python -c "from module_bank.encryption import Encryption; print(Encryption.generate_key())" > my_key.key
# 使用密钥打包
mbank pack my_module.py --name my_module --db modules.mbank --key "$(cat my_key.key)"
# 使用时提供密钥
mbank install --db modules.mbank --key "$(cat my_key.key)"
把密钥存进包(可选,注意权衡)
packer_key / get_key 可以把打包密钥写进包里,省掉"分发时要另传密钥"的麻烦:
packer = PythonToSQLite("modules.mbank", key=aes_key)
packer.pack_directory("src/")
packer.packer_key(aes_key) # 存进包根部(是 packer_key 文件,不是模块)
packer.close()
# 使用方拿到包就能直接跑(不用再传 key)
packer = PythonToSQLite("modules.mbank", readonly=True)
key = packer.get_key() # 没存过返回 None
packer.install_importer(key=key)
先把话说清楚:密钥和密文躺在同一个文件里,AES 这一层对"包泄露"就不再是 保护——能读包的人就能解开。它的价值是省事,真正的保护来自 PyArmor 那一层。 要真保密,就别用这两个方法,让使用方自己持有 key。
安全建议
- 密钥管理:加密密钥不要硬编码在代码中,使用环境变量或密钥管理服务
- 删除源代码:打包后可从数据库删除源代码,仅保留混淆后的代码:
# 删除所有模块的源代码
mbank delete_source --db modules.mbank
# 删除指定模块的源代码
mbank delete_source --db modules.mbank --module my_module
📖 详细使用
命令行工具
# 打包单个模块
mbank pack my_module.py --name my_module --db modules.mbank
# 打包整个目录(自动识别包结构)
mbank pack my_package/ --db modules.mbank
# 使用 PyArmor 混淆打包
mbank pack my_module.py --name my_module --db modules.mbank --pyarmor
# 使用 AES 加密打包
mbank pack my_module.py --name my_module --db modules.mbank --key "your-key"
# 双层保护
mbank pack my_module.py --name my_module --db modules.mbank --pyarmor --key "your-key"
# 列出数据库中的模块
mbank list --db modules.mbank
# 删除源代码
mbank delete_source --db modules.mbank
mbank delete_source --db modules.mbank --module my_module
# 安装导入器并进入交互模式
mbank install --db modules.mbank
mbank install --db modules.mbank --key "your-key"
编程接口
打包模块
from module_bank import PythonToSQLite
from module_bank.encryption import Encryption
# 生成加密密钥
key = Encryption.generate_key()
# 使用 AES 加密打包
packer = PythonToSQLite("modules.mbank", key=key)
# 或使用 PyArmor 混淆打包
packer = PythonToSQLite(
"modules.mbank",
pyarmor_obfuscate=True,
pyarmor_obf_code=1,
pyarmor_obf_mod=1,
)
# 打包单个模块
packer.pack_module("module.py", "module_name")
# 打包整个目录(自动识别包结构)
packer.pack_directory("my_package/")
# 验证包结构
packer.verify_package_structure()
导入模块
from module_bank import PythonToSQLite
# 如果数据库中的模块被 AES 加密了,需要提供密钥
packer = PythonToSQLite("modules.mbank")
# 安装导入器到 sys.meta_path
finder = packer.install_importer(key="your-key")
# 列出所有可用模块
modules = packer.list_modules()
for module in modules:
package_flag = " [包]" if module["is_package"] else ""
print(f" - {module['module_name']}{package_flag}")
# 直接导入数据库中的模块(就像普通模块一样)
import my_package
import my_package.submodule
print(my_package)
my_package.hello()
🏗️ 架构设计
核心组件
src/module_bank/
├── layout.py # 包内布局:模块名 ↔ svfs 路径的唯一映射处
├── python_to_sqlite.py # 主打包类
├── sqlite_module_importer.py # 存储管理器(写侧:模块与 PyArmor 运行时)
├── sqlite_meta_path_finder.py # 元路径查找器(只读打开 + 模块清单缓存)
├── sqlite_module_loader.py # 模块加载器(读包、解密、部署运行时)
├── encryption.py # AES 密钥管理
├── pyarmor_encryption.py # PyArmor 混淆封装
├── cli.py # 命令行接口
└── __init__.py # 模块导出
数据流
1. 打包阶段:
.py 文件 → (可选: PyArmor 混淆) → 编译为字节码 → (可选: AES 加密)
→ 作为文件写进 svfs 包(modules/ 下)
2. 导入阶段:
导入请求 → MetaPathFinder 查内存缓存 → ModuleLoader 读包
→ (AES 解密) → (PyArmor 运行时部署到临时目录) → 执行模块
包内布局
.mbank 是一个标准 svfs 包(schema v3),把它 unpack 出来就是下面这棵树——
所以"包里有什么"看一眼目录就知道:
bank.mbank
├─ modules/a/b.pyc 字节码(marshal,可选 AES 加密)← 模块存在的锚点
├─ modules/a/b.py 源码(PyArmor 混淆后的原文;可删)
├─ modules/a/b.meta.json 标志位与用户元数据(全默认值时不写这个文件)
├─ modules/pkg/__init__.pyc 包(is_package)用 __init__
├─ modules/pkg/__init__.py
├─ modules/pkg/__init__.meta.json
├─ runtime/pyarmor_runtime_000000/pyarmor_runtime.pyd
└─ runtime/pyarmor_runtime_000000/__init__.py
几条约定:
-
点分模块名里的点换成目录分隔符:
a.b→modules/a/b,所以包的字节码落在modules/a/b/__init__.pyc,与普通模块modules/a/b.pyc不冲突;is_package完全由路径决定(不记在元数据里,免得两处说法打架) -
.pyc是锚点:每个模块一定有字节码(打包时总是从源码编译),源码和元数据 都是可选的——delete_source删掉的就是.py那个文件 -
元数据文件里只有不可推导的信息:
{ "format": 1, "encrypted": false, "pyarmor_protected": true, "pyarmor_runtime": "pyarmor_runtime_000000", "metadata": { "version": "1.0" } }
-
一个模块的源码 + 元数据 + 字节码写在同一个事务里,所以不会出现"新元数据配 旧字节码"的中间态
用 svfs 的工具链直接操作包
因为 .mbank 就是 svfs 包,下列操作不需要 module_bank 参与:
svfs info bank.mbank # 统计与快照信息
vfsverify bank.mbank # 校验内容摘要
vfsunpack bank.mbank ./out # 导出成上面那棵目录树
svfs sign bank.mbank --key "$K" # 给快照做 HMAC 签名(防篡改)
from sqlite_vfs.core import SQLiteVFS
vfs = SQLiteVFS("bank.mbank", readonly=True)
print(vfs.get_stats())
print([i["name"] for i in vfs.list_directory("modules")])
vfs.close()
🔧 高级功能
排除模式
# 打包时排除特定文件
packer.pack_directory(
"my_project/",
exclude_patterns=["*_test.py", "*.pyc", "__pycache__"],
)
元数据存储
# 为模块添加元数据
packer.importer.add_module(
"my_module",
source_code,
is_package=False,
metadata={"version": "1.0", "author": "me"},
)
混合导入
# 数据库导入器默认插入到 meta_path 开头,优先级高于文件系统
import sys
from module_bank import PythonToSQLite
packer = PythonToSQLite("modules.mbank")
finder = packer.install_importer()
# 如果需要文件系统优先,将查找器追加到末尾
sys.meta_path.remove(finder)
sys.meta_path.append(finder)
验证运行时完整性
from module_bank.sqlite_module_importer import SQLiteModuleImporter
importer = SQLiteModuleImporter("modules.mbank")
runtimes = importer.list_runtimes()
for rt in runtimes:
print(f"运行时: {rt['runtime_name']} (创建于: {rt['created_at']})")
⚠️ 注意事项
安全性
- 模块字节码直接执行,确保包来源可信
- AES 加密密钥必须安全保管,丢失后将无法解密模块
- PyArmor 混淆提供源代码级别的保护,但商业版才能去除试用版水印
- 生产环境建议 PyArmor 混淆 + AES 加密同时使用
delete_source会把源码文件及其内容从包里删掉(svfs 会回收不再被引用的内容), 但和所有 SQLite 删除一样,这不是安全擦除:旧内容可能还留在数据库的空闲页里。 真要彻底消除,打一个不含源码的新包,或者打完之后用VACUUM压一遍
兼容性
- 0.4.0 起存储层换成 sqlite_vfs:
.mbank从"自建的表结构"变成 svfs 包 (modules/+runtime/文件树)。旧包不兼容,需要用mbank pack重新打包—— 旧包被打开时会直接报错说明这一点,不会静默给出一个空包 - 字节码不跨 Python 版本兼容(3.12 编译的模块无法在 3.11 加载)
- 不支持 C 扩展模块
- PyArmor 运行时文件与操作系统相关(Windows 用
.pyd,Linux 用.so) - 运行时文件需在目标系统上重新打包
- Python 3.10+(跟着 sqlite-vfs 与本项目实际用到的语法走)
性能
- 启动时:一次遍历包内容建模块清单(纯内存查表,与包大小无关),外加字节码 反序列化/运行时部署开销
- 运行时:与传统导入性能相同(使用 sys.modules 缓存)
- 最佳适用:长期运行的服务、桌面应用
更新模块
# 重新打包会自动更新(同一批文件在一个事务里替换,不会出现新旧混搭)
packer.pack_module("updated_module.py", "module_name")
删除模块
没有单独的"删模块"命令:不再需要的模块用 svfs 直接删掉目录项即可。更常见的
做法是删掉源码只留字节码(见上面的 delete_source)——那是一份 .mbank 该
有的状态。
from sqlite_vfs.core import SQLiteVFS
vfs = SQLiteVFS("modules.mbank")
vfs.remove("modules/module_to_remove.pyc") # 锚点没了,模块即不存在
vfs.remove("modules/module_to_remove.py")
vfs.close()
备份与恢复
# 包是单个文件,易于备份
cp modules.mbank modules.backup.mbank
# 恢复
cp modules.backup.mbank modules.mbank
版本管理
包里的版本就是 svfs 的快照(版本之间共享内容 blob,多留一版几乎不占空间)。 分工是:sqlite_vfs 只负责"把你要的那一版内容给你",选哪一版、什么时候留版 是 module_bank 这一层的事。
定格一个版本
from module_bank import PythonToSQLite
packer = PythonToSQLite("modules.mbank")
packer.pack_directory("src/")
packer.tag_version("release-1.0") # 把当前内容定格成 release-1.0
packer.close()
# 之后继续改、继续打包,写的是自动派生的工作版本(名字叫 work),
# release-1.0 那份不再变动
packer = PythonToSQLite("modules.mbank")
packer.pack_directory("src/")
packer.tag_version("release-2.0")
for v in packer.list_versions():
print(v["id"], v["name"], v["total_files"], "当前" if v["current"] else "")
packer.close()
为什么
tag_version不是简单调一次create_snapshot(name):svfs 的快照 语义是"当前那一版"(写入落进当前版本),所以create_snapshot("release-1.0", copy_entries=True)得到的名字落在会继续变动的那一版上——实测结果是打完 v1、tag 成 release-1.0、再打 v2 之后,release-1.0里是 v2。tag_version因此是"先把当前快照改名(定格),再派生一个工作版本接手后续写入"。
从指定版本加载
# 加载 release-1.0 那一份(id 或名字都行)
packer = PythonToSQLite("modules.mbank", readonly=True, snapshot="release-1.0")
packer.install_importer() # 之后 import 到的都是 v1.0 的代码
packer.close()
不指定 snapshot 就是跟着最新(工作版本)走;指定了就钉死在那一版,
包后续新增多少版本都不影响它——灰度、回滚、给老客户重发旧版都用这个。
snapshot 也可以传给 install_importer(snapshot=...),或者在包里用
use_version(...) 切换当前版本。可写句柄不能直接打开一个历史版本(那是
"顺手改掉发出去的那份"):确实要改,先显式 use_version("release-1.0")。
mbank list --db modules.mbank --snapshot release-1.0 # 看某一版里有什么
mbank install --db modules.mbank --snapshot release-1.0
防篡改:签名与验签
签的是快照摘要(HMAC-SHA256,对称密钥,只挡"发出后被改过",不挡伪造):
packer = PythonToSQLite("modules.mbank", readonly=True)
packer.sign_version("共享密钥", key_id="release-1.0") # 或先切到那一版再签
report = packer.verify_version("共享密钥") # {"ok": bool, "errors": [...]}
packer.close()
验不验签是加载方的策略,默认不验(不传就当没这回事):
# 加载前先验,验不过拒绝加载(抛 SignatureVerificationError)
finder = packer.install_importer(
signature_key="共享密钥",
signature_key_id="release-1.0", # 可选:只认这个 key_id 的签名
allow_unsigned=False, # 那一版还没签过时是否放行(默认放行)
)
mbank install --db modules.mbank --signature-key "$KEY" --require-signature
verify_version / install_importer 的验签都在只读连接上完成,不需要写权限。
更细的操作(快照清单、导出、发 CDN)直接用 svfs 的工具链:
svfs info modules.mbank # 统计与当前快照
vfsverify modules.mbank # 校验内容摘要
vfsunpack modules.mbank ./out # 导出成目录树
svfs sign modules.mbank --key "$K" # 命令行签名
📚 应用场景
- 商业软件分发 - PyArmor 混淆保护源代码知识产权
- 安全敏感环境 - AES 加密确保即使数据库泄露也无法还原代码
- 插件系统 - 动态加载数据库中的加密/混淆插件模块
- 教育平台 - 安全分发练习代码,防止学生抄袭
- 微服务 - 打包多个服务模块到单个加密文件
- 嵌入式系统 - 减少文件系统依赖,提高安全性
注意: 本工具主要用于模块分发和部署场景,不适合开发阶段的频繁修改。
许可证: MIT License
Metadata
Release files for module-bank 0.4.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| module_bank-0.4.1-py3-none-any.whl | Python 3 | none | any | Details |
Release files / module_bank-0.4.1-py3-none-any.whl
| Download URL | module_bank-0.4.1-py3-none-any.whl |
|---|---|
| Size | 44.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
b18b6078d79fd5d6b7f9cace08aad5f96917159718bba4f53d4ce277c2061b1b
|
|
BLAKE2b-256 checksum How to use checksums |
16dee9a714cdf40aa8b95599edc11e064194741986b5d7a30689524531c7c15c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.13
|