Skip to main content

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 导入系统
  • 完整包支持 - 支持包结构和子模块导入
  • 第三方依赖随包分发 - 自动扫描 import 或显式指定,把打包机上已安装的第三方 包(含 C 扩展、数据文件、.dist-info)一起收进包里的 packages/;运行时首次 解包到用户缓存目录,之后直接复用,目标机不需要再安装这些依赖
  • 内容去重与压缩 - 内容按 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"

连同第三方依赖一起打包

依赖需要先装在打包环境里(工具不联网、不执行 pip,只从已安装的包里收文件):

# 自动扫描源码里的 import,把自己写的包用到、且已安装的第三方依赖一起收进来
mbank pack my_package --db my_package.mbank --with-deps

# 显式指定(发行版名或导入名都行;默认沿 Requires-Dist 收传递依赖)
mbank pack my_package --db my_package.mbank --dep requests --dep yaml

# 从 requirements.txt 读依赖名(同样要求已安装)
mbank pack my_package --db my_package.mbank --requirements requirements.txt

打包结果可以自检:

mbank deps --db my_package.mbank      # 列出随包分发的第三方依赖及版本

依赖只在第一次运行时解压到用户缓存目录,后续进程按内容指纹复用同一份目录, 不再重复写盘:

mbank install --db my_package.mbank --cache-dir /var/cache/myapp   # 自定义缓存位置
mbank install --db my_package.mbank --no-cache                     # 关闭缓存(临时目录)
mbank cache                                                        # 查看缓存占用
mbank cache --clear                                                # 清理缓存

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。

安全建议

  1. 密钥管理:加密密钥不要硬编码在代码中,使用环境变量或密钥管理服务
  2. 删除源代码:打包后可从数据库删除源代码,仅保留混淆后的代码:
# 删除所有模块的源代码
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"

# 连同第三方依赖一起打包(自动扫描 import)
mbank pack my_package/ --db modules.mbank --with-deps

# 显式指定依赖(可重复),或从 requirements.txt 读
mbank pack my_package/ --db modules.mbank --dep requests --dep idna
mbank pack my_package/ --db modules.mbank --requirements requirements.txt

# 排除某个依赖(例如平台专属、运行环境自带的包)
mbank pack my_package/ --db modules.mbank --with-deps --dep-exclude pywin32

# 列出数据库中的模块
mbank list --db modules.mbank

# 列出随包分发的第三方依赖
mbank deps --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"

# 依赖解包缓存(默认用户缓存目录;--no-cache 关闭)
mbank install --db modules.mbank --cache-dir /var/cache/myapp
mbank install --db modules.mbank --no-cache

# 查看/清理依赖缓存
mbank cache
mbank cache --clear

编程接口

打包模块

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
from module_bank.dependencies import find_third_party_imports

packer = PythonToSQLite("modules.mbank")

# 显式指定:发行版名、导入名、requirements.txt 路径,或它们的列表
packer.pack_dependencies(["requests", "yaml"], recursive=True)

# 自动扫描自己的源码,把已安装的第三方依赖一起收进来
detected = find_third_party_imports(
    ["my_package/"],                 # 目录或 .py 文件
    packed_top_levels=["my_package"],  # 自己的包不算第三方
)
packer.pack_dependencies(optional=detected)   # 找不到只提示,不报错

# 排除某些依赖(支持发行版名或导入名)
packer.pack_dependencies(["requests"], exclude=["pywin32"])

# 看包里到底带了什么
for dependency in packer.list_dependencies():
    print(dependency["distribution"], dependency["version"],
          dependency["top_level"], dependency["files"])

requirements= 里的名字找不到会直接抛 DependencyError(名字写错不该静默产出 一个缺依赖的包);optional= 里找不到只打印提示(代码里的可选导入很常见)。

导入模块

from module_bank import PythonToSQLite

# 如果数据库中的模块被 AES 加密了,需要提供密钥
packer = PythonToSQLite("modules.mbank")

# 安装导入器到 sys.meta_path
finder = packer.install_importer(key="your-key")

# 依赖解包缓存:默认用户缓存目录(首次运行解压,之后直接复用)
finder = packer.install_importer(cache_dir="/var/cache/myapp")
# 关闭缓存:每次解到临时目录,finder.close()/进程退出时删除
finder = packer.install_importer(cache=False)

# 列出所有可用模块
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         # 主打包类
├── dependencies.py             # 第三方依赖:发现、文件收集、依赖清单
├── dependency_cache.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/ 下)

   第三方依赖(packages/):
   已安装的发行版 → 按 RECORD/顶层目录收文件(.py / .pyd / 数据 / dist-info)
                 → 原样写进 svfs 包(packages/ 下)+ 依赖清单

2. 导入阶段:
   导入请求 → MetaPathFinder 查内存缓存 → ModuleLoader 读包
            → (AES 解密) → (PyArmor 运行时部署到临时目录) → 执行模块

   第一次 import 到某个第三方依赖:
   按依赖内容指纹查缓存 → 命中则直接复用;未命中解出 packages/<顶层名>/ + dist-info
   → 用户缓存目录(deps-<hash>)→ sys.path → 标准路径查找器加载
   (原生扩展、数据文件、importlib.metadata 都按真实文件工作)

包内布局

.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
├─ packages/requests/__init__.py        第三方依赖:按安装时的文件原样存
├─ packages/requests-2.34.2.dist-info/  依赖元数据(importlib.metadata 用)
├─ packages/.manifest.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()

🔧 高级功能

打包第三方依赖

它做了什么

packages/ 是打包机 site-packages 的一个裁剪镜像:每个被选中的发行版按 RECORD 收齐它自己的文件(.py、.pyd/.so、数据文件、licenses), 连同它的 <name>-<version>.dist-info/ 一起写进包。运行时第一次 import 到某个 依赖的顶层包时,把这棵子树解到临时目录、挂到 sys.path 最前面,之后就是标准 Python 导入——C 扩展能加载、importlib.resources 能读数据、 importlib.metadata.version() 能取到版本号。

依赖清单(packages/.manifest.json)记录每个发行版的版本、顶层名与文件列表, 供 mbank deps 展示,也用于增量替换:同一个发行版重新打包时,先写新版本, 再按旧清单删掉新版本已经没有的文件,不会留下旧的 dist-info。

怎么选依赖

  • --with-deps:静态扫描源码里的 import / from ... import / 字面量 importlib.import_module("x"),排掉标准库和自己的包,剩下的作为候选。只收 打包环境里已经安装的包,不联网、不执行 pip。
  • --dep NAME:显式指定,发行版名(PyYAML)或导入名(yaml)都行;找不到 直接报错。
  • --requirements FILE:读 requirements.txt 里的名字(-r、--index-url 等 选项忽略)。
  • 默认沿 Requires-Dist 递归收传递依赖(--no-dep-recursive 关掉); extra == "..." 标记的可选依赖不收。
  • --dep-exclude NAME:排除某个依赖,应对平台专属或运行环境自带的包。

运行时行为与限制

  • 依赖第一次被 import 时解到用户缓存目录,之后进程直接复用,不再重复解压: Windows 是 %LOCALAPPDATA%\module_bank\cache,macOS 是 ~/Library/Caches/module_bank,Linux 是 $XDG_CACHE_HOME/module_bank (默认 ~/.cache/module_bank)。缓存目录按依赖内容指纹命名(deps-<hash>), 包内容、Python 版本或平台一变就自动换新目录,不会误用旧缓存。
  • 用 --cache-dir / install_importer(cache_dir=...) 改位置,MBANK_CACHE_DIR 改默认根目录;--no-cache / cache=False 回到临时目录模式(关闭时删除)。
  • 缓存目录是用户私有的(POSIX 0700);不可写时会自动回退到临时目录并给出 警告,导入本身不受影响。
  • 并发冷启动是安全的:文件先写临时名再原子替换,完成标记用 O_EXCL 创建,两个 进程最多重复解一遍,不会读到半份文件。
  • 缓存不会自动删除;用 mbank cache 查看占用、mbank cache --clear 清理。 缓存模式不删除已加载的 .pyd,Windows 上"运行时锁定文件"的问题也就不存在了。
  • 依赖是明文:第三方代码本来就是公开的,这一层不做 PyArmor/AES;保护只 针对你自己放在 modules/ 里的模块。
  • 包里的依赖是为打包平台与 Python 版本准备的:带 C 扩展的依赖(numpy、 cryptography…)不能跨系统/跨版本直接用,需要在目标平台重新打包。
  • 自动扫描是静态的:动态拼接的 import(importlib.import_module(name) 的变量 形式)发现不了,用 --dep 补。
  • 不会安装 console script(依赖自带的命令行工具);entry_points 元数据会随 dist-info 一起带上,但入口脚本本身没有安装。
  • .pth 文件、安装器写入 site-packages 之外的脚本/数据不在收集范围内。
  • 子进程里 import 依赖时,需要子进程自己也调用 install_importer()—— sys.path 的改动是进程内的。
  • 同名冲突:modules/ 里你自己的模块优先;依赖之间同名顶层包按 sys.path 顺序(后解出来的包在前)。

排除模式

# 打包时排除特定文件
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 重新打包—— 旧包被打开时会直接报错说明这一点,不会静默给出一个空包
  • 0.5.0 起支持第三方依赖:依赖解析结果存在 packages/ + 依赖清单里;没有 清单的旧包行为不变,不需要重新打包
  • 0.5.1 起依赖解包缓存:首次运行解到用户缓存目录,之后按内容指纹复用; cache=False / --no-cache 可回到每次解临时目录的模式
  • 字节码不跨 Python 版本兼容(3.12 编译的模块无法在 3.11 加载)
  • 你自己的模块不能是 C 扩展(打包时按源码编译为字节码);第三方依赖可以 连同 C 扩展一起打包
  • 打包的第三方依赖与平台、Python 版本相关:带 C 扩展的依赖只能用于对应平台 (如 Windows 的 .pyd 不能拿到 Linux 用),需要在目标平台重新打包
  • PyArmor 运行时文件与操作系统相关(Windows 用 .pyd,Linux 用 .so)
  • PyArmor 运行时与第三方依赖都需在目标系统上重新打包
  • 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")   # 签名要写入签名记录 → 可写句柄
packer.sign_version("共享密钥", key_id="ci-2026", snapshot="release-1.0")
packer.close()

key_id 是"用哪把密钥签的"(同一版可以有多把密钥的签名,靠它区分),要签哪一版 用 snapshot=。验签(包括"加载前先验")在只读句柄上就能做:

packer = PythonToSQLite("modules.mbank", readonly=True)
report = packer.verify_version("共享密钥", snapshot="release-1.0", key_id="ci-2026")
print(report["ok"], report["errors"])            # {"ok": bool, "errors": [...]}
packer.close()

验不验签是加载方的策略,默认不验(不传就当没这回事):

# 加载前先验,验不过拒绝加载(抛 SignatureVerificationError)
finder = packer.install_importer(
    signature_key="共享密钥",
    signature_key_id="ci-2026",       # 可选:只认这个 key_id 的签名
    allow_unsigned=False,             # 那一版还没签过时是否放行(默认放行)
)
mbank install --db modules.mbank --signature-key "$KEY" \
    --signature-key-id ci-2026 --require-signature

install_importer 的结论落在返回的 finder 上(finder.signature_report):报告里 signature 为 None 表示那一版没有匹配的签名记录(按了 key_id 就只认那个 key_id 的记录),默认策略下放行;这与"签名不匹配"是两回事——后者一律拒绝加载。 传了 signature_key 不等于验过了,脚本里要如实报,就看这个字段。

上面这些验签(verify_version、install_importer)都跑在只读连接上,不需要写权限。 更细的操作(快照清单、导出、发 CDN)直接用 svfs 的工具链:

svfs info modules.mbank               # 统计与当前快照
vfsverify modules.mbank               # 校验内容摘要
vfsunpack modules.mbank ./out         # 导出成目录树
svfs sign modules.mbank --key "$K" --key-id ci-2026   # 命令行签名(等价)

📚 应用场景

  1. 商业软件分发 - PyArmor 混淆保护源代码知识产权
  2. 安全敏感环境 - AES 加密确保即使数据库泄露也无法还原代码
  3. 插件系统 - 动态加载数据库中的加密/混淆插件模块
  4. 教育平台 - 安全分发练习代码,防止学生抄袭
  5. 微服务 - 打包多个服务模块到单个加密文件
  6. 嵌入式系统 - 减少文件系统依赖,提高安全性

注意: 本工具主要用于模块分发和部署场景,不适合开发阶段的频繁修改。

许可证: MIT License

Metadata

Release files for module-bank 0.5.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Built distribution (wheel)

Table of built distributions (wheels) for module-bank 0.5.1
File Interpreter ABI Platform
module_bank-0.5.1-py3-none-any.whl Python 3 none any Details

Release files / module_bank-0.5.1-py3-none-any.whl

Download URL module_bank-0.5.1-py3-none-any.whl
Size 71.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
673bafe41643ca93c7a3da457834b8d3563f898455961d471d8ff3c44b1c5e5f
BLAKE2b-256 checksum
How to use checksums
843ea131606e5c9b12fd25bbbf7055e1892faaecb5f1e30f22759112b5ca91de
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.13

Security for module-bank 0.5.1

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page