modify: pyarmor 加密支持
This commit is contained in:
@@ -12,3 +12,6 @@ wheels/
|
||||
output.md
|
||||
log/
|
||||
*.mb
|
||||
*.mbank
|
||||
dual_protection_
|
||||
*.db
|
||||
@@ -1,15 +1,16 @@
|
||||
# Python Module Bank - SQLite 模块打包系统
|
||||
|
||||
一个将Python模块打包到SQLite数据库,并支持从数据库直接导入模块的工具系统。
|
||||
一个将 Python 模块打包到 SQLite 数据库,并支持从数据库直接导入模块的工具系统。支持 **AES 字节码加密** 和 **PyArmor 代码混淆** 双重保护机制。
|
||||
|
||||
## 🌟 特性
|
||||
|
||||
- **单文件分发** - 将所有模块打包到单个SQLite数据库文件
|
||||
- **源代码保护** - 模块以编译后的字节码形式存储
|
||||
- **动态导入** - 运行时直接从数据库加载模块,无需文件系统
|
||||
- **单文件分发** - 将所有模块打包到单个 SQLite 数据库文件
|
||||
- **AES 字节码加密** - 编译后的字节码使用 Fernet (AES-128-CBC) 加密存储
|
||||
- **PyArmor 代码混淆** - 支持使用 PyArmor 对 Python 源代码进行混淆保护
|
||||
- **双层保护** - 可同时使用 PyArmor 混淆 + AES 字节码加密,提供双重安全保障
|
||||
- **动态导入** - 运行时直接从数据库加载模块,无缝集成 Python 导入系统
|
||||
- **完整包支持** - 支持包结构和子模块导入
|
||||
- **CLI工具** - 提供完整的命令行接口
|
||||
- **导入钩子** - 无缝集成Python导入系统
|
||||
- **CLI 工具** - 提供完整的命令行接口
|
||||
|
||||
## 📦 安装
|
||||
|
||||
@@ -21,69 +22,154 @@ cd module-bank
|
||||
pip install -e .
|
||||
```
|
||||
|
||||
### 依赖要求
|
||||
### 使用 uv(推荐)
|
||||
|
||||
- Python 3.7+
|
||||
- 无需额外依赖(仅使用标准库)
|
||||
```bash
|
||||
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 源代码级别的混淆保护
|
||||
- 商业版可去除试用版水印
|
||||
|
||||
## 🚀 快速开始
|
||||
|
||||
### 1. 创建示例模块
|
||||
|
||||
```python
|
||||
# my_package/my_module.py
|
||||
# my_package/__init__.py
|
||||
def hello():
|
||||
print("Hello from my_module!")
|
||||
print("Hello from my_package!")
|
||||
return "success"
|
||||
|
||||
VERSION = "1.0.0"
|
||||
```
|
||||
|
||||
```python
|
||||
# my_package/utils.py
|
||||
def add(a, b):
|
||||
return a + b
|
||||
```
|
||||
|
||||
### 2. 打包模块到数据库
|
||||
|
||||
```python
|
||||
# pack_example.py
|
||||
from module_bank import PythonToSQLite
|
||||
from module_bank.encryption import Encryption
|
||||
#### 使用 AES 加密(推荐)
|
||||
|
||||
key = Encryption.generate_key()
|
||||
```bash
|
||||
# 生成密钥并打包
|
||||
mbank pack my_package --db my_package.mbank --key "$(python -c "from module_bank.encryption import Encryption; print(Encryption.generate_key())")"
|
||||
```
|
||||
|
||||
with open("my_package.key", "w") as key_file:
|
||||
key_file.write(key)
|
||||
#### 使用 PyArmor 混淆
|
||||
|
||||
packer = PythonToSQLite("my_package.mbank", key)
|
||||
packer.pack_directory("my_package")
|
||||
```bash
|
||||
# 使用 PyArmor 保护源代码
|
||||
mbank pack my_package --db my_package.mbank --pyarmor
|
||||
```
|
||||
|
||||
#### 使用双层保护
|
||||
|
||||
```bash
|
||||
# PyArmor 混淆 + AES 加密
|
||||
mbank pack my_package --db my_package.mbank --pyarmor --key "your-encryption-key"
|
||||
```
|
||||
|
||||
### 3. 从数据库导入
|
||||
|
||||
```python
|
||||
from module_bank import PythonToSQLite
|
||||
|
||||
with open("my_package.key") as f:
|
||||
key = f.read()
|
||||
# import_example.py
|
||||
from module_bank.python_to_sqlite import PythonToSQLite
|
||||
|
||||
packer = PythonToSQLite("my_package.mbank")
|
||||
finder = packer.install_importer(key)
|
||||
finder = packer.install_importer(key="your-encryption-key") # 如果加密了需要提供密钥
|
||||
|
||||
from my_package import my_module
|
||||
from my_package import utils
|
||||
from my_package import hello
|
||||
|
||||
print(my_module)
|
||||
my_module.hello()
|
||||
print(hello())
|
||||
print(utils.add(3, 4))
|
||||
```
|
||||
|
||||
## 🛡️ 安全保护机制
|
||||
|
||||
### 第一层:PyArmor 代码混淆
|
||||
|
||||
PyArmor 对 Python 源代码进行混淆,将代码转换为难以阅读和逆向的形式:
|
||||
|
||||
- 混淆后的代码依赖运行时二进制文件(`.pyd`/`.so`),无法直接运行
|
||||
- 支持混淆级别控制(代码混淆、模块混淆)
|
||||
- 运行时文件会自动存储在数据库中,加载时自动部署
|
||||
|
||||
```bash
|
||||
# 高级混淆选项
|
||||
mbank pack my_module.py --name my_module --db modules.db \
|
||||
--pyarmor \
|
||||
--pyarmor-obf-code 1 \
|
||||
--pyarmor-obf-mod 1
|
||||
```
|
||||
|
||||
### 第二层:AES 字节码加密
|
||||
|
||||
编译后的字节码使用 Fernet (AES-128-CBC) 加密:
|
||||
|
||||
```bash
|
||||
# 生成密钥
|
||||
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.db --key "$(cat my_key.key)"
|
||||
|
||||
# 使用时提供密钥
|
||||
mbank install --db modules.db --key "$(cat my_key.key)"
|
||||
```
|
||||
|
||||
### 安全建议
|
||||
|
||||
1. **密钥管理**:加密密钥不要硬编码在代码中,使用环境变量或密钥管理服务
|
||||
2. **删除源代码**:打包后可从数据库删除源代码,仅保留混淆后的代码:
|
||||
|
||||
```bash
|
||||
# 删除所有模块的源代码
|
||||
mbank delete_source --db modules.db
|
||||
|
||||
# 删除指定模块的源代码
|
||||
mbank delete_source --db modules.db --module my_module
|
||||
```
|
||||
|
||||
## 📖 详细使用
|
||||
|
||||
### 命令行工具
|
||||
|
||||
```python
|
||||
# 打包模块或目录
|
||||
mbank pack my_package --db modules.db --key my_package_key
|
||||
```bash
|
||||
# 打包单个模块
|
||||
mbank pack my_module.py --name my_module --db modules.db
|
||||
|
||||
# 打包整个目录(自动识别包结构)
|
||||
mbank pack my_package/ --db modules.db
|
||||
|
||||
# 使用 PyArmor 混淆打包
|
||||
mbank pack my_module.py --name my_module --db modules.db --pyarmor
|
||||
|
||||
# 使用 AES 加密打包
|
||||
mbank pack my_module.py --name my_module --db modules.db --key "your-key"
|
||||
|
||||
# 双层保护
|
||||
mbank pack my_module.py --name my_module --db modules.db --pyarmor --key "your-key"
|
||||
|
||||
# 列出数据库中的模块
|
||||
mbank list --db modules.db
|
||||
|
||||
# 删除源代码
|
||||
mbank delete_source --db modules.db
|
||||
mbank delete_source --db modules.db --module my_module
|
||||
|
||||
# 安装导入器并进入交互模式
|
||||
mbank install --db modules.db --key my_package_key
|
||||
mbank install --db modules.db
|
||||
mbank install --db modules.db --key "your-key"
|
||||
```
|
||||
|
||||
### 编程接口
|
||||
@@ -92,8 +178,21 @@ mbank install --db modules.db --key my_package_key
|
||||
|
||||
```python
|
||||
from module_bank import PythonToSQLite
|
||||
from module_bank.encryption import Encryption
|
||||
|
||||
packer = PythonToSQLite("modules.mbank")
|
||||
# 生成加密密钥
|
||||
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")
|
||||
@@ -109,21 +208,25 @@ packer.verify_package_structure()
|
||||
|
||||
```python
|
||||
from module_bank import PythonToSQLite
|
||||
import sys
|
||||
|
||||
# 如果数据库中的模块被 AES 加密了,需要提供密钥
|
||||
packer = PythonToSQLite("modules.mbank")
|
||||
|
||||
# 安装导入器到sys.meta_path
|
||||
finder = packer.install_importer()
|
||||
# 安装导入器到 sys.meta_path
|
||||
finder = packer.install_importer(key="your-key")
|
||||
|
||||
# 列出所有可用模块
|
||||
modules = packer.list_modules()
|
||||
for module in modules:
|
||||
print(f"{module['module_name']} {'[包]' if module['is_package'] else ''}")
|
||||
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()
|
||||
```
|
||||
|
||||
## 🏗️ 架构设计
|
||||
@@ -132,37 +235,49 @@ import my_package.submodule
|
||||
|
||||
```python
|
||||
src/module_bank/
|
||||
├── python_to_sqlite.py # 主打包类
|
||||
├── sqlite_module_importer.py # 数据库存储管理器
|
||||
├── sqlite_meta_path_finder.py # 元路径查找器
|
||||
├── sqlite_module_loader.py # 模块加载器
|
||||
├── encryption.py # 密钥管理
|
||||
├── cli.py # 命令行接口
|
||||
└── __init__.py # 模块导出
|
||||
├── python_to_sqlite.py # 主打包类
|
||||
├── sqlite_module_importer.py # 数据库存储管理器
|
||||
├── sqlite_meta_path_finder.py # 元路径查找器
|
||||
├── sqlite_module_loader.py # 模块加载器
|
||||
├── encryption.py # AES 密钥管理
|
||||
├── pyarmor_encryption.py # PyArmor 混淆封装
|
||||
├── cli.py # 命令行接口
|
||||
└── __init__.py # 模块导出
|
||||
```
|
||||
|
||||
### 数据流
|
||||
|
||||
```text
|
||||
1. 打包阶段:
|
||||
.py文件 → 编译为字节码 → 存储到SQLite数据库
|
||||
.py 文件 → (可选: PyArmor 混淆) → 编译为字节码 → (可选: AES 加密) → 存储到 SQLite
|
||||
|
||||
2. 导入阶段:
|
||||
导入请求 → MetaPathFinder查找 → ModuleLoader加载 → 执行模块
|
||||
导入请求 → MetaPathFinder 查找 → ModuleLoader 加载 → (AES 解密) → (PyArmor 运行时部署) → 执行模块
|
||||
```
|
||||
|
||||
### 数据库模式
|
||||
|
||||
```sql
|
||||
-- 模块表
|
||||
CREATE TABLE python_modules (
|
||||
module_name TEXT PRIMARY KEY,
|
||||
source_code TEXT, -- 源代码(可选)
|
||||
bytecode BLOB, -- 编译后的字节码
|
||||
is_package BOOLEAN, -- 是否是包
|
||||
metadata TEXT, -- 元数据(JSON格式)
|
||||
encrypted INTEGER DEFAULT 0, -- 是否已加密
|
||||
source_code TEXT, -- 源代码(PyArmor 混淆后)
|
||||
bytecode BLOB, -- 编译后的字节码(可选 AES 加密)
|
||||
is_package BOOLEAN, -- 是否是包
|
||||
metadata TEXT, -- 元数据(JSON 格式)
|
||||
encrypted INTEGER DEFAULT 0, -- AES 是否已加密
|
||||
pyarmor_protected INTEGER DEFAULT 0, -- 是否已 PyArmor 混淆
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
);
|
||||
|
||||
-- PyArmor 运行时存储表
|
||||
CREATE TABLE pyarmor_runtime (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
runtime_name TEXT UNIQUE NOT NULL,
|
||||
pyd_data BLOB, -- 原生二进制文件(.pyd/.so)
|
||||
init_data TEXT, -- __init__.py 内容
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
```
|
||||
|
||||
## 🔧 高级功能
|
||||
@@ -173,7 +288,7 @@ CREATE TABLE python_modules (
|
||||
# 打包时排除特定文件
|
||||
packer.pack_directory(
|
||||
"my_project/",
|
||||
exclude_patterns=["*_test.py", "*.pyc", "__pycache__"]
|
||||
exclude_patterns=["*_test.py", "*.pyc", "__pycache__"],
|
||||
)
|
||||
```
|
||||
|
||||
@@ -185,42 +300,56 @@ packer.importer.add_module(
|
||||
"my_module",
|
||||
source_code,
|
||||
is_package=False,
|
||||
metadata={"version": "1.0", "author": "me"}
|
||||
metadata={"version": "1.0", "author": "me"},
|
||||
)
|
||||
```
|
||||
|
||||
### 混合导入
|
||||
|
||||
```python
|
||||
# 可以同时使用文件系统和数据库导入
|
||||
# 数据库导入器优先级更高
|
||||
# 数据库导入器默认插入到 meta_path 开头,优先级高于文件系统
|
||||
import sys
|
||||
from module_bank import PythonToSQLite
|
||||
|
||||
packer = PythonToSQLite("modules.db")
|
||||
finder = packer.install_importer() # 插入到meta_path开头
|
||||
finder = packer.install_importer()
|
||||
|
||||
# 如果需要文件系统优先,可以调整插入位置
|
||||
# 如果需要文件系统优先,将查找器追加到末尾
|
||||
sys.meta_path.remove(finder)
|
||||
sys.meta_path.append(finder)
|
||||
```
|
||||
|
||||
### 验证运行时完整性
|
||||
|
||||
```python
|
||||
from module_bank.sqlite_module_importer import SQLiteModuleImporter
|
||||
|
||||
importer = SQLiteModuleImporter("modules.db")
|
||||
runtimes = importer.list_runtimes()
|
||||
for rt in runtimes:
|
||||
print(f"运行时: {rt['runtime_name']} (创建于: {rt['created_at']})")
|
||||
```
|
||||
|
||||
## ⚠️ 注意事项
|
||||
|
||||
### 安全性
|
||||
|
||||
- 模块字节码直接执行,确保数据库来源可信
|
||||
- 生产环境建议添加代码签名验证
|
||||
- AES 加密密钥必须安全保管,丢失后将无法解密模块
|
||||
- PyArmor 混淆提供源代码级别的保护,但商业版才能去除试用版水印
|
||||
- 生产环境建议 PyArmor 混淆 + AES 加密同时使用
|
||||
|
||||
### 兼容性
|
||||
|
||||
- 字节码不跨Python版本兼容
|
||||
- 不支持C扩展模块
|
||||
- 不支持需要文件系统资源的模块(如__file__依赖)
|
||||
- 字节码不跨 Python 版本兼容(3.12 编译的模块无法在 3.11 加载)
|
||||
- 不支持 C 扩展模块
|
||||
- PyArmor 运行时文件与操作系统相关(Windows 用 `.pyd`,Linux 用 `.so`)
|
||||
- 运行时文件需在目标系统上重新打包
|
||||
|
||||
### 性能
|
||||
|
||||
- **启动时**:有一次性数据库查询和反序列化开销
|
||||
- **运行时**:与传统导入性能相同(使用sys.modules缓存)
|
||||
- **启动时**:有一次性数据库查询和反序列化/运行时部署开销
|
||||
- **运行时**:与传统导入性能相同(使用 sys.modules 缓存)
|
||||
- **最佳适用**:长期运行的服务、桌面应用
|
||||
|
||||
### 更新模块
|
||||
@@ -249,11 +378,15 @@ cp modules.backup.mbank modules.mbank
|
||||
|
||||
## 📚 应用场景
|
||||
|
||||
1. 商业软件分发 - 保护源代码知识产权
|
||||
2. 插件系统 - 动态加载数据库中的插件模块
|
||||
3. 教育平台 - 安全分发练习代码
|
||||
4. 微服务 - 打包多个服务模块到单个文件
|
||||
5. 嵌入式系统 - 减少文件系统依赖
|
||||
1. **商业软件分发** - PyArmor 混淆保护源代码知识产权
|
||||
2. **安全敏感环境** - AES 加密确保即使数据库泄露也无法还原代码
|
||||
3. **插件系统** - 动态加载数据库中的加密/混淆插件模块
|
||||
4. **教育平台** - 安全分发练习代码,防止学生抄袭
|
||||
5. **微服务** - 打包多个服务模块到单个加密文件
|
||||
6. **嵌入式系统** - 减少文件系统依赖,提高安全性
|
||||
|
||||
---
|
||||
|
||||
**注意**: 本工具主要用于模块分发和部署场景,不适合开发阶段的频繁修改。
|
||||
|
||||
**许可证**: MIT License
|
||||
|
||||
@@ -0,0 +1,202 @@
|
||||
"""
|
||||
PyArmor + AES 双层保护完整示例
|
||||
|
||||
本示例演示如何同时使用 PyArmor 混淆和 AES 字节码加密
|
||||
来提供双重保护。
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import tempfile
|
||||
import shutil
|
||||
|
||||
# 添加项目源码路径
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
|
||||
|
||||
# ========== 1. 创建一个需要保护的模块 ==========
|
||||
|
||||
SECRET_MODULE_CODE = """
|
||||
# 这是需要保护的敏感代码(支付逻辑、授权验证等)
|
||||
import hashlib
|
||||
import hmac
|
||||
|
||||
SECRET_KEY = "this-is-a-top-secret-key"
|
||||
|
||||
def verify_license(license_key: str) -> bool:
|
||||
\"\"\"验证许可证密钥(敏感业务逻辑)\"\"\"
|
||||
expected = hmac.new(
|
||||
SECRET_KEY.encode(),
|
||||
license_key.encode(),
|
||||
hashlib.sha256
|
||||
).hexdigest()
|
||||
return expected.startswith("a1b2")
|
||||
|
||||
def calculate_commission(amount: float, level: int) -> float:
|
||||
\"\"\"计算佣金(商业机密算法)\"\"\"
|
||||
rates = {1: 0.1, 2: 0.15, 3: 0.25, 4: 0.35}
|
||||
rate = rates.get(level, 0.05)
|
||||
# 敏感计算公式
|
||||
commission = amount * rate * (1.0 - 0.05)
|
||||
return round(commission, 2)
|
||||
|
||||
class PaymentProcessor:
|
||||
\"\"\"支付处理器(核心商业逻辑)\"\"\"
|
||||
def __init__(self, merchant_id: str):
|
||||
self.merchant_id = merchant_id
|
||||
|
||||
def process(self, order_id: str, amount: float) -> dict:
|
||||
# 模拟支付处理
|
||||
return {
|
||||
"status": "success",
|
||||
"order_id": order_id,
|
||||
"amount": amount,
|
||||
"merchant": self.merchant_id,
|
||||
"transaction_id": hashlib.sha256(
|
||||
f"{order_id}{self.merchant_id}".encode()
|
||||
).hexdigest()[:16]
|
||||
}
|
||||
|
||||
VERSION = "2.1.0"
|
||||
BUILD = "2026-05-04"
|
||||
"""
|
||||
|
||||
|
||||
def main():
|
||||
print("=" * 70)
|
||||
print("PyArmor + AES 双层保护完整示例")
|
||||
print("=" * 70)
|
||||
|
||||
# 创建临时工作目录
|
||||
# work_dir = tempfile.mkdtemp(prefix="dual_protection_")
|
||||
# db_path = os.path.join(work_dir, "protected_modules.db")
|
||||
db_path = os.path.join("dual_protection_", "protected_modules.db")
|
||||
key_file = os.path.join("dual_protection_", "encryption_key.txt")
|
||||
|
||||
try:
|
||||
# ========== 步骤 1: 引入模块 ==========
|
||||
from module_bank.encryption import Encryption
|
||||
from module_bank.python_to_sqlite import PythonToSQLite
|
||||
|
||||
# ========== 步骤 2: 生成 AES 加密密钥 ==========
|
||||
print("\n[步骤 1/5] 生成 AES 加密密钥...")
|
||||
aes_key = Encryption.generate_key()
|
||||
with open(key_file, "w") as f:
|
||||
f.write(aes_key)
|
||||
print(f" AES 密钥已生成: {aes_key[:20]}...")
|
||||
|
||||
# ========== 步骤 3: 使用双层保护打包模块 ==========
|
||||
print("\n[步骤 2/5] 使用 PyArmor + AES 双层保护打包模块...")
|
||||
packer = PythonToSQLite(
|
||||
db_path,
|
||||
key=aes_key, # AES 字节码加密
|
||||
pyarmor_obfuscate=True, # PyArmor 源代码混淆
|
||||
pyarmor_obf_code=1, # 启用代码混淆
|
||||
pyarmor_obf_mod=1, # 启用模块混淆
|
||||
)
|
||||
|
||||
packer.importer.add_module(
|
||||
"payment_system",
|
||||
SECRET_MODULE_CODE,
|
||||
is_package=False,
|
||||
metadata={
|
||||
"version": "2.1.0",
|
||||
"protection": "pyarmor_obfuscation + aes_encryption",
|
||||
},
|
||||
)
|
||||
print(" ✓ 模块已使用双层保护存储到数据库")
|
||||
|
||||
# ========== 步骤 4: 验证数据库中的保护状态 ==========
|
||||
print("\n[步骤 3/5] 验证数据库中的保护状态...")
|
||||
cursor = packer.importer.connection.execute(
|
||||
"""SELECT module_name, encrypted, pyarmor_protected, source_code,
|
||||
LENGTH(bytecode) as bytecode_size,
|
||||
LENGTH(source_code) as source_size
|
||||
FROM python_modules"""
|
||||
)
|
||||
row = cursor.fetchone()
|
||||
print(f" 模块名: {row['module_name']}")
|
||||
print(f" AES 加密: {'✓ 是' if row['encrypted'] else '✗ 否'}")
|
||||
print(f" PyArmor 混淆: {'✓ 是' if row['pyarmor_protected'] else '✗ 否'}")
|
||||
print(f" 字节码大小: {row['bytecode_size']} 字节")
|
||||
print(f" 源代码大小: {row['source_size']} 字节")
|
||||
|
||||
# 验证 PyArmor 运行时也已存储
|
||||
runtimes = packer.importer.list_runtimes()
|
||||
for rt in runtimes:
|
||||
print(f" PyArmor 运行时: {rt['runtime_name']}")
|
||||
|
||||
# 显示数据库中存储的混淆代码片段
|
||||
print("\n[验证] 数据库中的混淆代码(前200字符):")
|
||||
obfuscated = row["source_code"]
|
||||
print(f" {obfuscated[:200]}...")
|
||||
|
||||
# ========== 步骤 5: 从数据库导入并执行 ==========
|
||||
print("\n[步骤 4/5] 从数据库导入模块(需要提供 AES 密钥)...")
|
||||
# 清除之前的导入
|
||||
if "payment_system" in sys.modules:
|
||||
del sys.modules["payment_system"]
|
||||
|
||||
# 使用密钥安装导入器
|
||||
finder = packer.install_importer(key=aes_key)
|
||||
|
||||
# 导入模块
|
||||
import importlib
|
||||
|
||||
mod = importlib.import_module("payment_system")
|
||||
|
||||
print(f" ✓ 模块导入成功: {mod}")
|
||||
print(f" 模块文件: {mod.__file__}")
|
||||
|
||||
# ========== 步骤 6: 验证模块功能正常 ==========
|
||||
print("\n[步骤 5/5] 验证模块功能...")
|
||||
|
||||
# 调用验证许可证函数
|
||||
result = mod.verify_license("TEST-1234-5678")
|
||||
print(f" verify_license('TEST-1234-5678') → {result}")
|
||||
|
||||
# 调用佣金计算
|
||||
commission = mod.calculate_commission(10000.0, 3)
|
||||
print(f" calculate_commission(10000, 3) → ¥{commission}")
|
||||
|
||||
# 使用支付处理器
|
||||
processor = mod.PaymentProcessor("MERCHANT_001")
|
||||
payment_result = processor.process("ORDER_20260504", 299.00)
|
||||
print(f" PaymentProcessor.process() → {payment_result['status']}")
|
||||
print(f" 交易ID: {payment_result['transaction_id']}")
|
||||
|
||||
# 访问模块常量
|
||||
print(f"\n 模块版本: {mod.VERSION}")
|
||||
print(f" 构建日期: {mod.BUILD}")
|
||||
|
||||
# ========== 安全验证 ==========
|
||||
print("\n" + "=" * 70)
|
||||
print("安全验证")
|
||||
print("=" * 70)
|
||||
print("""
|
||||
1. PyArmor 混淆: 源代码已转换为不可读的混淆代码
|
||||
2. AES 加密: 字节码已使用 AES-128-CBC 加密
|
||||
3. 运行时保护: PyArmor 运行时二进制文件无法直接加载
|
||||
4. 双重解密: 需要先 AES 解密 → 再通过 PyArmor 运行时执行
|
||||
|
||||
即使有人获取了数据库文件:
|
||||
- 没有 AES 密钥 → 无法解密字节码
|
||||
- 没有 PyArmor 运行时 → 无法执行混淆代码
|
||||
- 两者同时具备 → 才能解密和执行
|
||||
""")
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n❌ 错误: {e}")
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
finally:
|
||||
# 清理
|
||||
print("\n[清理] 清除临时文件...")
|
||||
# shutil.rmtree(work_dir, ignore_errors=True)
|
||||
if "payment_system" in sys.modules:
|
||||
del sys.modules["payment_system"]
|
||||
print(" 完成!")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,57 @@
|
||||
from module_bank import PythonToSQLite
|
||||
|
||||
with open("./dual_protection_/encryption_key.txt") as f:
|
||||
key = f.read()
|
||||
|
||||
packer = PythonToSQLite(
|
||||
"./dual_protection_/protected_modules.db",
|
||||
key=key,
|
||||
pyarmor_obfuscate=True,
|
||||
pyarmor_obf_code=1,
|
||||
pyarmor_obf_mod=1,
|
||||
)
|
||||
|
||||
packer.install_importer()
|
||||
|
||||
import payment_system # type: ignore
|
||||
|
||||
mod = payment_system
|
||||
print(f" ✓ 模块导入成功: {mod}")
|
||||
print(f" 模块文件: {mod.__file__}")
|
||||
|
||||
# ========== 步骤 6: 验证模块功能正常 ==========
|
||||
print("\n[步骤 5/5] 验证模块功能...")
|
||||
|
||||
# 调用验证许可证函数
|
||||
result = mod.verify_license("TEST-1234-5678")
|
||||
print(f" verify_license('TEST-1234-5678') → {result}")
|
||||
|
||||
# 调用佣金计算
|
||||
commission = mod.calculate_commission(10000.0, 3)
|
||||
print(f" calculate_commission(10000, 3) → ¥{commission}")
|
||||
|
||||
# 使用支付处理器
|
||||
processor = mod.PaymentProcessor("MERCHANT_001")
|
||||
payment_result = processor.process("ORDER_20260504", 299.00)
|
||||
print(f" PaymentProcessor.process() → {payment_result['status']}")
|
||||
print(f" 交易ID: {payment_result['transaction_id']}")
|
||||
|
||||
# 访问模块常量
|
||||
print(f"\n 模块版本: {mod.VERSION}")
|
||||
print(f" 构建日期: {mod.BUILD}")
|
||||
|
||||
# ========== 安全验证 ==========
|
||||
print("\n" + "=" * 70)
|
||||
print("安全验证")
|
||||
print("=" * 70)
|
||||
print("""
|
||||
1. PyArmor 混淆: 源代码已转换为不可读的混淆代码
|
||||
2. AES 加密: 字节码已使用 AES-128-CBC 加密
|
||||
3. 运行时保护: PyArmor 运行时二进制文件无法直接加载
|
||||
4. 双重解密: 需要先 AES 解密 → 再通过 PyArmor 运行时执行
|
||||
|
||||
即使有人获取了数据库文件:
|
||||
- 没有 AES 密钥 → 无法解密字节码
|
||||
- 没有 PyArmor 运行时 → 无法执行混淆代码
|
||||
- 两者同时具备 → 才能解密和执行
|
||||
""")
|
||||
+2
-1
@@ -1,12 +1,13 @@
|
||||
[project]
|
||||
dependencies = [
|
||||
"cryptography>=45.0.7",
|
||||
"pyarmor>=9.2.4",
|
||||
]
|
||||
description = "Add your description here"
|
||||
name = "module_bank"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.7"
|
||||
version = "0.2.3"
|
||||
version = "0.3.0"
|
||||
|
||||
[[tool.uv.index]]
|
||||
default = true
|
||||
|
||||
@@ -0,0 +1,295 @@
|
||||
"""
|
||||
集成测试:测试 PyArmor 加密打包 -> 存储 -> 加载 -> 执行 全流程
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import tempfile
|
||||
import shutil
|
||||
|
||||
# 添加项目模块路径
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
|
||||
|
||||
TEST_DB = os.path.join(os.path.dirname(__file__), "test_pyarmor_integration.db")
|
||||
|
||||
|
||||
def create_test_module():
|
||||
"""创建测试用的 Python 模块"""
|
||||
return """
|
||||
def greet(name):
|
||||
return f"Hello, {name}! This is a PyArmor protected module."
|
||||
|
||||
class Calculator:
|
||||
def add(self, a, b):
|
||||
return a + b
|
||||
|
||||
def multiply(self, a, b):
|
||||
return a * b
|
||||
|
||||
VERSION = "1.0.0"
|
||||
"""
|
||||
|
||||
|
||||
def test_pyarmor_obfuscate():
|
||||
"""测试 PyArmor 混淆功能"""
|
||||
print("=" * 60)
|
||||
print("测试 1: PyArmor 混淆功能")
|
||||
print("=" * 60)
|
||||
|
||||
from module_bank.pyarmor_encryption import PyArmorEncryption
|
||||
|
||||
enc = PyArmorEncryption(obf_code=1, obf_mod=1)
|
||||
|
||||
# 检查 PyArmor 是否可用
|
||||
if not enc.check_pyarmor_available():
|
||||
print("ERROR: PyArmor 未安装或不可用")
|
||||
return False
|
||||
|
||||
source = create_test_module()
|
||||
print(f"原始代码长度: {len(source)} 字节")
|
||||
|
||||
try:
|
||||
obfuscated_code, runtime_data = enc.obfuscate_source(
|
||||
source, "test_protected_module"
|
||||
)
|
||||
print(f"混淆后代码长度: {len(obfuscated_code)} 字节")
|
||||
print(f"运行时文件: {'有' if runtime_data else '无'}")
|
||||
print(f"运行时数据大小: {len(runtime_data) if runtime_data else 0} 字节")
|
||||
|
||||
# 验证是否被保护
|
||||
assert enc.is_pyarmor_protected(obfuscated_code), "代码应被标记为 PyArmor 保护"
|
||||
print(f"PyArmor 保护标记: 正确")
|
||||
|
||||
# 提取运行时模块名
|
||||
runtime_name = enc.extract_runtime_info(obfuscated_code)
|
||||
print(f"运行时模块名: {runtime_name}")
|
||||
|
||||
print("测试 1 通过!\n")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"ERROR: {e}")
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
|
||||
def test_pack_with_pyarmor():
|
||||
"""测试使用 PyArmor 打包到 SQLite"""
|
||||
print("=" * 60)
|
||||
print("测试 2: 使用 PyArmor 打包到 SQLite")
|
||||
print("=" * 60)
|
||||
|
||||
# 清理旧的测试数据库
|
||||
if os.path.exists(TEST_DB):
|
||||
os.remove(TEST_DB)
|
||||
|
||||
try:
|
||||
from module_bank.python_to_sqlite import PythonToSQLite
|
||||
|
||||
# 使用 PyArmor 创建打包器
|
||||
packer = PythonToSQLite(
|
||||
TEST_DB,
|
||||
pyarmor_obfuscate=True,
|
||||
pyarmor_obf_code=1,
|
||||
pyarmor_obf_mod=1,
|
||||
)
|
||||
|
||||
source = create_test_module()
|
||||
packer.importer.add_module("test_protected_module", source, is_package=False)
|
||||
print("模块已打包到数据库")
|
||||
|
||||
# 验证模块已存储
|
||||
modules = packer.list_modules()
|
||||
module_names = [m["module_name"] for m in modules]
|
||||
assert "test_protected_module" in module_names, "模块应在数据库中存在"
|
||||
print(f"数据库中的模块: {module_names}")
|
||||
|
||||
# 验证运行时已存储
|
||||
runtimes = packer.importer.list_runtimes()
|
||||
print(f"数据库中的运行时: {[r['runtime_name'] for r in runtimes]}")
|
||||
|
||||
# 验证 pyarmor_protected 标记
|
||||
cursor = packer.importer.connection.execute(
|
||||
"SELECT pyarmor_protected FROM python_modules WHERE module_name = ?",
|
||||
("test_protected_module",),
|
||||
)
|
||||
row = cursor.fetchone()
|
||||
assert row["pyarmor_protected"] == 1, "模块应标记为 pyarmor_protected"
|
||||
print("PyArmor 保护标记: 已正确设置")
|
||||
|
||||
print("测试 2 通过!\n")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"ERROR: {e}")
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
|
||||
def test_import_pyarmor_module():
|
||||
"""测试从 SQLite 导入并执行 PyArmor 保护的模块"""
|
||||
print("=" * 60)
|
||||
print("测试 3: 从 SQLite 导入并执行 PyArmor 保护模块")
|
||||
print("=" * 60)
|
||||
|
||||
try:
|
||||
from module_bank.python_to_sqlite import PythonToSQLite
|
||||
from module_bank.sqlite_meta_path_finder import SQLiteMetaPathFinder
|
||||
|
||||
packer = PythonToSQLite(TEST_DB)
|
||||
|
||||
# 安装导入器
|
||||
finder = packer.install_importer()
|
||||
|
||||
# 尝试导入模块
|
||||
import importlib
|
||||
|
||||
mod = importlib.import_module("test_protected_module")
|
||||
print(f"模块导入成功: {mod}")
|
||||
print(f"模块名: {mod.__name__}")
|
||||
print(f"模块文件: {mod.__file__}")
|
||||
|
||||
# 调用模块函数
|
||||
result = mod.greet("World")
|
||||
print(f"调用 greet('World'): {result}")
|
||||
assert result == "Hello, World! This is a PyArmor protected module."
|
||||
|
||||
# 使用模块类
|
||||
calc = mod.Calculator()
|
||||
assert calc.add(3, 4) == 7
|
||||
assert calc.multiply(3, 4) == 12
|
||||
print(f"Calculator.add(3, 4) = {calc.add(3, 4)}")
|
||||
print(f"Calculator.multiply(3, 4) = {calc.multiply(3, 4)}")
|
||||
|
||||
# 访问模块变量
|
||||
print(f"VERSION = {mod.VERSION}")
|
||||
assert mod.VERSION == "1.0.0"
|
||||
|
||||
print("测试 3 通过!\n")
|
||||
|
||||
# 清理
|
||||
sys.meta_path.remove(finder)
|
||||
if "test_protected_module" in sys.modules:
|
||||
del sys.modules["test_protected_module"]
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"ERROR: {e}")
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
|
||||
def test_cli_with_pyarmor():
|
||||
"""测试 CLI 的 PyArmor 参数"""
|
||||
print("=" * 60)
|
||||
print("测试 4: CLI --pyarmor 参数")
|
||||
print("=" * 60)
|
||||
|
||||
try:
|
||||
import tempfile
|
||||
|
||||
# 创建临时源文件
|
||||
tmpdir = tempfile.mkdtemp()
|
||||
src_file = os.path.join(tmpdir, "hello_module.py")
|
||||
with open(src_file, "w", encoding="utf-8") as f:
|
||||
f.write("def hello():\n return 'Hello from PyArmor CLI!'\n")
|
||||
|
||||
db_path = os.path.join(tmpdir, "test_cli.db")
|
||||
|
||||
# 测试解析 --pyarmor 参数
|
||||
from module_bank.cli import main as cli_main
|
||||
|
||||
# 保存原始argv
|
||||
old_argv = sys.argv
|
||||
try:
|
||||
sys.argv = [
|
||||
"mbank",
|
||||
"pack",
|
||||
src_file,
|
||||
"--db",
|
||||
db_path,
|
||||
"--name",
|
||||
"hello_cli_module",
|
||||
"--pyarmor",
|
||||
]
|
||||
cli_main()
|
||||
print("CLI pack with --pyarmor 执行成功")
|
||||
|
||||
# 验证模块已用 PyArmor 保护
|
||||
from module_bank.sqlite_module_importer import SQLiteModuleImporter
|
||||
|
||||
importer = SQLiteModuleImporter(db_path)
|
||||
cursor = importer.connection.execute(
|
||||
"SELECT pyarmor_protected FROM python_modules WHERE module_name = ?",
|
||||
("hello_cli_module",),
|
||||
)
|
||||
row = cursor.fetchone()
|
||||
assert row and row["pyarmor_protected"] == 1, "CLI 打包的模块应启用 PyArmor"
|
||||
print("CLI 打包的模块已正确启用 PyArmor 保护")
|
||||
|
||||
# 验证运行时存储
|
||||
runtimes = importer.list_runtimes()
|
||||
assert len(runtimes) > 0, "CLI 打包后应有运行时文件存储在数据库中"
|
||||
print(f"运行时已存储: {[r['runtime_name'] for r in runtimes]}")
|
||||
|
||||
finally:
|
||||
sys.argv = old_argv
|
||||
shutil.rmtree(tmpdir, ignore_errors=True)
|
||||
|
||||
print("测试 4 通过!\n")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"ERROR: {e}")
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
|
||||
def cleanup():
|
||||
"""清理测试文件"""
|
||||
if os.path.exists(TEST_DB):
|
||||
os.remove(TEST_DB)
|
||||
print(f"已清理测试数据库: {TEST_DB}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("\n" + "=" * 60)
|
||||
print("PyArmor 集成测试套件")
|
||||
print("=" * 60 + "\n")
|
||||
|
||||
tests = [
|
||||
("PyArmor 混淆功能", test_pyarmor_obfuscate),
|
||||
("PyArmor 打包到 SQLite", test_pack_with_pyarmor),
|
||||
("从 SQLite 导入 PyArmor 模块", test_import_pyarmor_module),
|
||||
("CLI --pyarmor 参数", test_cli_with_pyarmor),
|
||||
]
|
||||
|
||||
passed = 0
|
||||
failed = 0
|
||||
|
||||
for name, func in tests:
|
||||
print(f"\n>>> 运行测试: {name}")
|
||||
try:
|
||||
if func():
|
||||
passed += 1
|
||||
else:
|
||||
failed += 1
|
||||
except Exception as e:
|
||||
print(f"测试异常: {e}")
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
failed += 1
|
||||
|
||||
cleanup()
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print(f"测试完成: {passed} 通过, {failed} 失败")
|
||||
print("=" * 60)
|
||||
|
||||
sys.exit(1 if failed > 0 else 0)
|
||||
@@ -0,0 +1,2 @@
|
||||
# Pyarmor 9.2.4 (trial), 000000, 2026-05-04T17:52:33.319452
|
||||
from .pyarmor_runtime import __pyarmor__
|
||||
Binary file not shown.
File diff suppressed because one or more lines are too long
@@ -0,0 +1,53 @@
|
||||
"""测试 PyArmor 加密功能"""
|
||||
|
||||
import tempfile
|
||||
import os
|
||||
import sys
|
||||
import shutil
|
||||
|
||||
# 确保能导入项目模块
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
|
||||
|
||||
|
||||
def test_pyarmor_encrypt():
|
||||
"""测试 PyArmor encrypt_script 功能"""
|
||||
import pyarmor.pyarmor as pa
|
||||
|
||||
tmpdir = tempfile.mkdtemp()
|
||||
try:
|
||||
src = os.path.join(tmpdir, "test_mod.py")
|
||||
with open(src, "w", encoding="utf-8") as f:
|
||||
f.write('def hello():\n print("Hello from PyArmor!")\n')
|
||||
|
||||
dest = os.path.join(tmpdir, "dist", "test_mod.py")
|
||||
os.makedirs(os.path.dirname(dest), exist_ok=True)
|
||||
|
||||
# Check capsule
|
||||
capsule = pa.DEFAULT_CAPSULE
|
||||
print(f"Capsule: {capsule}")
|
||||
print(f"Capsule exists: {os.path.exists(capsule)}")
|
||||
|
||||
# Try encrypt
|
||||
result = pa.encrypt_script(
|
||||
pubkey=capsule,
|
||||
filename=src,
|
||||
destname=dest,
|
||||
)
|
||||
print(f"Result: {result}")
|
||||
|
||||
# Check output
|
||||
if os.path.exists(dest):
|
||||
with open(dest, "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
print(f"Output length: {len(content)} bytes")
|
||||
print(f"First 300 chars:\n{content[:300]}")
|
||||
print(f"\n--- Full content ({len(content)} bytes) ---")
|
||||
print(content)
|
||||
else:
|
||||
print(f"ERROR: Output file not created at {dest}")
|
||||
finally:
|
||||
shutil.rmtree(tmpdir, ignore_errors=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_pyarmor_encrypt()
|
||||
@@ -2,10 +2,12 @@ from .python_to_sqlite import PythonToSQLite
|
||||
from .sqlite_module_importer import SQLiteModuleImporter
|
||||
from .sqlite_module_loader import SQLiteModuleLoader
|
||||
from .sqlite_meta_path_finder import SQLiteMetaPathFinder
|
||||
from .pyarmor_encryption import PyArmorEncryption
|
||||
|
||||
__all__ = [
|
||||
"PythonToSQLite",
|
||||
"SQLiteModuleImporter",
|
||||
"SQLiteModuleLoader",
|
||||
"SQLiteMetaPathFinder",
|
||||
"PyArmorEncryption",
|
||||
]
|
||||
|
||||
+37
-1
@@ -14,6 +14,25 @@ def main():
|
||||
pack_parser.add_argument(
|
||||
"--key", help="加密密钥(Fernet格式,32字节base64),若不提供则不加密"
|
||||
)
|
||||
pack_parser.add_argument(
|
||||
"--pyarmor",
|
||||
action="store_true",
|
||||
help="使用 PyArmor 混淆保护源代码",
|
||||
)
|
||||
pack_parser.add_argument(
|
||||
"--pyarmor-obf-code",
|
||||
type=int,
|
||||
default=1,
|
||||
choices=[0, 1],
|
||||
help="PyArmor 代码混淆级别 (0=禁用, 1=启用, 默认: 1)",
|
||||
)
|
||||
pack_parser.add_argument(
|
||||
"--pyarmor-obf-mod",
|
||||
type=int,
|
||||
default=1,
|
||||
choices=[0, 1],
|
||||
help="PyArmor 模块混淆级别 (0=禁用, 1=启用, 默认: 1)",
|
||||
)
|
||||
|
||||
# 列出命令
|
||||
list_parser = subparsers.add_parser("list", help="列出数据库中的模块")
|
||||
@@ -40,7 +59,13 @@ def main():
|
||||
if args.command == "pack":
|
||||
from .python_to_sqlite import PythonToSQLite
|
||||
|
||||
packer = PythonToSQLite(args.db, key=args.key)
|
||||
packer = PythonToSQLite(
|
||||
args.db,
|
||||
key=args.key,
|
||||
pyarmor_obfuscate=args.pyarmor,
|
||||
pyarmor_obf_code=args.pyarmor_obf_code,
|
||||
pyarmor_obf_mod=args.pyarmor_obf_mod,
|
||||
)
|
||||
|
||||
if Path(args.source).is_dir():
|
||||
packer.pack_directory(args.source)
|
||||
@@ -49,6 +74,17 @@ def main():
|
||||
packer.pack_module(args.source, args.name)
|
||||
print(f"已打包模块: {args.source}")
|
||||
|
||||
# 如果启用了 PyArmor,显示运行时信息
|
||||
if args.pyarmor:
|
||||
from .sqlite_module_importer import SQLiteModuleImporter
|
||||
|
||||
importer = SQLiteModuleImporter(args.db)
|
||||
runtimes = importer.list_runtimes()
|
||||
if runtimes:
|
||||
print(f"\nPyArmor 运行时信息:")
|
||||
for rt in runtimes:
|
||||
print(f" - {rt['runtime_name']} (创建于: {rt['created_at']})")
|
||||
|
||||
elif args.command == "list":
|
||||
from .python_to_sqlite import PythonToSQLite
|
||||
|
||||
|
||||
@@ -0,0 +1,202 @@
|
||||
"""
|
||||
PyArmor 加密支持模块
|
||||
|
||||
为 module_bank 提供 PyArmor 代码混淆/加密能力。
|
||||
使用 PyArmor CLI 对 Python 源代码进行混淆保护,
|
||||
并将混淆后的源代码和运行时文件存储到 SQLite 数据库中。
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
import shutil
|
||||
import subprocess
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Optional, Tuple
|
||||
|
||||
|
||||
class PyArmorEncryption:
|
||||
"""PyArmor 加密处理器,用于混淆/保护 Python 源代码"""
|
||||
|
||||
def __init__(self, key: Optional[str] = None, obf_code: int = 1, obf_mod: int = 1):
|
||||
"""
|
||||
初始化 PyArmor 加密处理器
|
||||
|
||||
Args:
|
||||
key: 加密密钥(可选,PyArmor 不使用此参数,保留接口兼容性)
|
||||
obf_code: 代码混淆级别 (0/1)
|
||||
obf_mod: 模块混淆级别 (0/1)
|
||||
"""
|
||||
self.key = key
|
||||
self.obf_code = obf_code
|
||||
self.obf_mod = obf_mod
|
||||
|
||||
def obfuscate_source(
|
||||
self, source_code: str, module_name: str
|
||||
) -> Tuple[str, Optional[dict]]:
|
||||
"""
|
||||
使用 PyArmor 混淆源代码
|
||||
|
||||
Args:
|
||||
source_code: 原始 Python 源代码
|
||||
module_name: 模块名
|
||||
|
||||
Returns:
|
||||
(混淆后的源代码, PyArmor 运行时数据字典)
|
||||
运行时数据字典包含: {
|
||||
"runtime_name": "pyarmor_runtime_000000",
|
||||
"pyd_data": bytes,
|
||||
"init_data": str,
|
||||
}
|
||||
为 None 表示没有运行时文件
|
||||
"""
|
||||
if not self.check_pyarmor_available():
|
||||
raise ImportError(
|
||||
"PyArmor 未安装。请使用 'uv add pyarmor' 或 'pip install pyarmor' 安装。"
|
||||
)
|
||||
|
||||
temp_dir = tempfile.mkdtemp(prefix=f"pyarmor_pack_")
|
||||
try:
|
||||
# 创建临时源文件
|
||||
src_filename = f"{module_name.replace('.', '_')}.py"
|
||||
src_file = os.path.join(temp_dir, src_filename)
|
||||
with open(src_file, "w", encoding="utf-8") as f:
|
||||
f.write(source_code)
|
||||
|
||||
# 输出目录
|
||||
output_dir = os.path.join(temp_dir, "dist")
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
|
||||
# 调用 pyarmor gen 命令进行混淆
|
||||
cmd = [
|
||||
sys.executable,
|
||||
"-m",
|
||||
"pyarmor.cli",
|
||||
"gen",
|
||||
"-O",
|
||||
output_dir,
|
||||
"-i",
|
||||
src_file,
|
||||
]
|
||||
|
||||
# 添加混淆级别参数
|
||||
if self.obf_code == 0:
|
||||
cmd.extend(["--obf-code", "0"])
|
||||
if self.obf_mod == 0:
|
||||
cmd.extend(["--obf-mod", "0"])
|
||||
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
cwd=temp_dir,
|
||||
)
|
||||
|
||||
if result.returncode != 0:
|
||||
raise RuntimeError(f"PyArmor 混淆失败:\n{result.stderr}")
|
||||
|
||||
# 读取混淆后的代码
|
||||
obfuscated_file = os.path.join(output_dir, src_filename)
|
||||
if not os.path.exists(obfuscated_file):
|
||||
raise RuntimeError(f"PyArmor 未生成输出文件: {obfuscated_file}")
|
||||
|
||||
with open(obfuscated_file, "r", encoding="utf-8") as f:
|
||||
obfuscated_code = f.read()
|
||||
|
||||
# 查找并读取运行时文件
|
||||
runtime_data = self._find_and_read_runtime(output_dir)
|
||||
|
||||
return obfuscated_code, runtime_data
|
||||
|
||||
except Exception as e:
|
||||
if isinstance(e, RuntimeError):
|
||||
raise
|
||||
raise RuntimeError(f"PyArmor 混淆失败 ({module_name}): {e}") from e
|
||||
finally:
|
||||
shutil.rmtree(temp_dir, ignore_errors=True)
|
||||
|
||||
def _find_and_read_runtime(self, output_dir: str) -> Optional[dict]:
|
||||
"""
|
||||
查找并读取 PyArmor 运行时文件
|
||||
|
||||
Args:
|
||||
output_dir: PyArmor 输出目录
|
||||
|
||||
Returns:
|
||||
包含运行时文件的字典: {
|
||||
"runtime_name": "pyarmor_runtime_000000",
|
||||
"pyd_data": bytes, # 原生二进制文件
|
||||
"init_data": str, # __init__.py 内容
|
||||
}
|
||||
如果没有找到则返回 None
|
||||
"""
|
||||
output_path = Path(output_dir)
|
||||
# 查找 pyarmor_runtime_* 目录
|
||||
runtime_dirs = list(output_path.glob("pyarmor_runtime_*"))
|
||||
if not runtime_dirs:
|
||||
return None
|
||||
|
||||
runtime_dir = runtime_dirs[0]
|
||||
runtime_name = runtime_dir.name
|
||||
|
||||
result = {"runtime_name": runtime_name, "pyd_data": None, "init_data": None}
|
||||
|
||||
# 查找 .pyd 或 .so 文件
|
||||
runtime_files = list(runtime_dir.glob("pyarmor_runtime.*"))
|
||||
if runtime_files:
|
||||
with open(runtime_files[0], "rb") as f:
|
||||
result["pyd_data"] = f.read()
|
||||
|
||||
# 读取 __init__.py
|
||||
init_file = runtime_dir / "__init__.py"
|
||||
if init_file.exists():
|
||||
with open(init_file, "r", encoding="utf-8") as f:
|
||||
result["init_data"] = f.read()
|
||||
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def extract_runtime_info(obfuscated_code: str) -> Optional[str]:
|
||||
"""
|
||||
从混淆后的代码中提取运行时模块名
|
||||
|
||||
Args:
|
||||
obfuscated_code: 混淆后的源代码
|
||||
|
||||
Returns:
|
||||
运行时模块名,如 'pyarmor_runtime_000000'
|
||||
"""
|
||||
match = re.search(
|
||||
r"from\s+(pyarmor_runtime_\w+)\s+import\s+__pyarmor__",
|
||||
obfuscated_code,
|
||||
)
|
||||
return match.group(1) if match else None
|
||||
|
||||
@staticmethod
|
||||
def is_pyarmor_protected(source_code: str) -> bool:
|
||||
"""
|
||||
检测源代码是否已被 PyArmor 保护
|
||||
|
||||
Args:
|
||||
source_code: 要检测的源代码
|
||||
|
||||
Returns:
|
||||
是否为 PyArmor 保护的代码
|
||||
"""
|
||||
markers = [
|
||||
"pyarmor_runtime",
|
||||
"__pyarmor__",
|
||||
"from pyarmor_runtime",
|
||||
]
|
||||
return any(marker in source_code for marker in markers)
|
||||
|
||||
@staticmethod
|
||||
def check_pyarmor_available() -> bool:
|
||||
"""检查 PyArmor 是否可用"""
|
||||
try:
|
||||
import pyarmor # noqa: F401
|
||||
|
||||
return True
|
||||
except ImportError:
|
||||
return False
|
||||
@@ -8,10 +8,26 @@ from .sqlite_meta_path_finder import SQLiteMetaPathFinder
|
||||
class PythonToSQLite:
|
||||
"""Python到SQLite的打包工具"""
|
||||
|
||||
def __init__(self, db_path: str = "python_modules.db", key: Optional[str] = None):
|
||||
def __init__(
|
||||
self,
|
||||
db_path: str = "python_modules.db",
|
||||
key: Optional[str] = None,
|
||||
pyarmor_obfuscate: bool = False,
|
||||
pyarmor_obf_code: int = 1,
|
||||
pyarmor_obf_mod: int = 1,
|
||||
):
|
||||
self.db_path = db_path
|
||||
self.key = key
|
||||
self.importer = SQLiteModuleImporter(db_path, key)
|
||||
self.pyarmor_obfuscate = pyarmor_obfuscate
|
||||
self.pyarmor_obf_code = pyarmor_obf_code
|
||||
self.pyarmor_obf_mod = pyarmor_obf_mod
|
||||
self.importer = SQLiteModuleImporter(
|
||||
db_path,
|
||||
key,
|
||||
pyarmor_obfuscate=pyarmor_obfuscate,
|
||||
pyarmor_obf_code=pyarmor_obf_code,
|
||||
pyarmor_obf_mod=pyarmor_obf_mod,
|
||||
)
|
||||
|
||||
def pack_directory(
|
||||
self,
|
||||
@@ -146,12 +162,13 @@ class PythonToSQLite:
|
||||
(module_name, source_code, bytecode, is_package, metadata, encrypted)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
("key", key, None, None, None, None),
|
||||
("key", None, key, None, None, None),
|
||||
)
|
||||
self.importer.connection.commit()
|
||||
|
||||
def get_key(self):
|
||||
cursor = self.importer.connection.execute("""
|
||||
SELECT source_code FROM python_modules WHERE module_name = 'key'
|
||||
SELECT bytecode FROM python_modules WHERE module_name = 'key'
|
||||
""")
|
||||
return cursor.fetchone()
|
||||
row = cursor.fetchone()
|
||||
return row["bytecode"] if row else None
|
||||
|
||||
@@ -1,25 +1,37 @@
|
||||
import sqlite3
|
||||
import marshal
|
||||
import base64
|
||||
import os
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Dict, Optional
|
||||
from typing import Dict, Optional, Tuple
|
||||
from .encryption import Encryption
|
||||
|
||||
|
||||
class SQLiteModuleImporter:
|
||||
"""从SQLite数据库导入Python模块的导入器"""
|
||||
|
||||
def __init__(self, db_path: str, key: Optional[str] = None):
|
||||
def __init__(
|
||||
self,
|
||||
db_path: str,
|
||||
key: Optional[str] = None,
|
||||
pyarmor_obfuscate: bool = False,
|
||||
pyarmor_obf_code: int = 1,
|
||||
pyarmor_obf_mod: int = 1,
|
||||
):
|
||||
self.db_path = db_path
|
||||
self.connection = sqlite3.connect(db_path)
|
||||
self.connection.row_factory = sqlite3.Row
|
||||
self.encryption = Encryption(key)
|
||||
self.pyarmor_obfuscate = pyarmor_obfuscate
|
||||
self.pyarmor_obf_code = pyarmor_obf_code
|
||||
self.pyarmor_obf_mod = pyarmor_obf_mod
|
||||
self._pyarmor_enc = None
|
||||
self._create_tables()
|
||||
|
||||
def _create_tables(self):
|
||||
"""创建存储模块的数据库表"""
|
||||
self.connection.execute(
|
||||
"""
|
||||
self.connection.execute("""
|
||||
CREATE TABLE IF NOT EXISTS python_modules (
|
||||
module_name TEXT PRIMARY KEY,
|
||||
source_code TEXT,
|
||||
@@ -28,17 +40,46 @@ class SQLiteModuleImporter:
|
||||
metadata TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
"""
|
||||
)
|
||||
""")
|
||||
try:
|
||||
self.connection.execute(
|
||||
"ALTER TABLE python_modules ADD COLUMN encrypted INTEGER DEFAULT 0"
|
||||
)
|
||||
except sqlite3.OperationalError:
|
||||
# 列可能已存在,忽略错误
|
||||
pass
|
||||
|
||||
# 添加 pyarmor_protected 列(用于标记 PyArmor 混淆过的模块)
|
||||
try:
|
||||
self.connection.execute(
|
||||
"ALTER TABLE python_modules ADD COLUMN pyarmor_protected INTEGER DEFAULT 0"
|
||||
)
|
||||
except sqlite3.OperationalError:
|
||||
pass
|
||||
|
||||
# 创建 PyArmor 运行时存储表(支持二进制文件和文本文件的存储)
|
||||
self.connection.execute("""
|
||||
CREATE TABLE IF NOT EXISTS pyarmor_runtime (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
runtime_name TEXT UNIQUE NOT NULL,
|
||||
pyd_data BLOB,
|
||||
init_data TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
""")
|
||||
self.connection.commit()
|
||||
|
||||
def _get_pyarmor_enc(self):
|
||||
"""惰性获取 PyArmor 加密器"""
|
||||
if self._pyarmor_enc is None:
|
||||
from .pyarmor_encryption import PyArmorEncryption
|
||||
|
||||
self._pyarmor_enc = PyArmorEncryption(
|
||||
key=None,
|
||||
obf_code=self.pyarmor_obf_code,
|
||||
obf_mod=self.pyarmor_obf_mod,
|
||||
)
|
||||
return self._pyarmor_enc
|
||||
|
||||
def add_module(
|
||||
self,
|
||||
module_name: str,
|
||||
@@ -47,6 +88,24 @@ class SQLiteModuleImporter:
|
||||
metadata: Dict | None = None,
|
||||
):
|
||||
"""添加模块到数据库"""
|
||||
pyarmor_protected = 0
|
||||
|
||||
# 如果启用了 PyArmor 混淆,则先混淆源代码
|
||||
if self.pyarmor_obfuscate:
|
||||
pyarmor_enc = self._get_pyarmor_enc()
|
||||
if pyarmor_enc.check_pyarmor_available():
|
||||
obfuscated_code, runtime_dict = pyarmor_enc.obfuscate_source(
|
||||
source_code, module_name
|
||||
)
|
||||
source_code = obfuscated_code
|
||||
pyarmor_protected = 1
|
||||
|
||||
# 存储运行时文件
|
||||
if runtime_dict is not None:
|
||||
self._store_runtime(runtime_dict)
|
||||
else:
|
||||
print(f"警告: PyArmor 未安装,跳过模块 '{module_name}' 的 PyArmor 混淆")
|
||||
|
||||
# 编译为字节码
|
||||
bytecode = compile(source_code, f"<sqlite_module:{module_name}>", "exec")
|
||||
bytecode_blob = marshal.dumps(bytecode)
|
||||
@@ -62,8 +121,8 @@ class SQLiteModuleImporter:
|
||||
self.connection.execute(
|
||||
"""
|
||||
INSERT OR REPLACE INTO python_modules
|
||||
(module_name, source_code, bytecode, is_package, metadata, encrypted)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
(module_name, source_code, bytecode, is_package, metadata, encrypted, pyarmor_protected)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
module_name,
|
||||
@@ -72,10 +131,49 @@ class SQLiteModuleImporter:
|
||||
is_package,
|
||||
metadata_str,
|
||||
encrypted_flag,
|
||||
pyarmor_protected,
|
||||
),
|
||||
)
|
||||
self.connection.commit()
|
||||
|
||||
def _store_runtime(self, runtime_dict: dict):
|
||||
"""存储 PyArmor 运行时文件到数据库"""
|
||||
self.connection.execute(
|
||||
"""
|
||||
INSERT OR REPLACE INTO pyarmor_runtime
|
||||
(runtime_name, pyd_data, init_data)
|
||||
VALUES (?, ?, ?)
|
||||
""",
|
||||
(
|
||||
runtime_dict.get("runtime_name"),
|
||||
runtime_dict.get("pyd_data"),
|
||||
runtime_dict.get("init_data"),
|
||||
),
|
||||
)
|
||||
self.connection.commit()
|
||||
|
||||
def get_runtime(self, runtime_name: str) -> Optional[dict]:
|
||||
"""从数据库获取 PyArmor 运行时文件数据"""
|
||||
cursor = self.connection.execute(
|
||||
"SELECT runtime_name, pyd_data, init_data FROM pyarmor_runtime WHERE runtime_name = ?",
|
||||
(runtime_name,),
|
||||
)
|
||||
row = cursor.fetchone()
|
||||
if row:
|
||||
return {
|
||||
"runtime_name": row["runtime_name"],
|
||||
"pyd_data": row["pyd_data"],
|
||||
"init_data": row["init_data"],
|
||||
}
|
||||
return None
|
||||
|
||||
def list_runtimes(self):
|
||||
"""列出数据库中的所有 PyArmor 运行时"""
|
||||
cursor = self.connection.execute(
|
||||
"SELECT runtime_name, created_at FROM pyarmor_runtime ORDER BY id"
|
||||
)
|
||||
return [dict(row) for row in cursor.fetchall()]
|
||||
|
||||
def add_module_from_file(self, file_path_str: str, module_name: str | None = None):
|
||||
"""从文件添加模块"""
|
||||
file_path = Path(file_path_str)
|
||||
|
||||
@@ -3,6 +3,11 @@ import importlib
|
||||
import importlib.abc
|
||||
import marshal
|
||||
import types
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
from .encryption import Encryption
|
||||
|
||||
|
||||
@@ -21,6 +26,7 @@ class SQLiteModuleLoader(importlib.abc.Loader):
|
||||
self.db_path = db_path
|
||||
self.db_connection.row_factory = sqlite3.Row
|
||||
self.encryption = Encryption(key)
|
||||
self._runtime_temp_dir = None
|
||||
|
||||
def create_module(self, spec):
|
||||
"""创建模块对象"""
|
||||
@@ -44,7 +50,7 @@ class SQLiteModuleLoader(importlib.abc.Loader):
|
||||
def exec_module(self, module): # type: ignore
|
||||
"""执行模块代码"""
|
||||
cursor = self.db_connection.execute(
|
||||
"SELECT bytecode, is_package, encrypted FROM python_modules WHERE module_name = ?",
|
||||
"SELECT bytecode, is_package, encrypted, pyarmor_protected, source_code FROM python_modules WHERE module_name = ?",
|
||||
(self.module_name,),
|
||||
)
|
||||
row = cursor.fetchone()
|
||||
@@ -52,6 +58,12 @@ class SQLiteModuleLoader(importlib.abc.Loader):
|
||||
if not row:
|
||||
raise ImportError(f"No module named '{self.module_name}' in database")
|
||||
|
||||
pyarmor_protected = row["pyarmor_protected"]
|
||||
|
||||
# 如果是 PyArmor 保护的模块,通过源代码执行(需要先部署运行时)
|
||||
if pyarmor_protected:
|
||||
return self._exec_pyarmor_module(module, row)
|
||||
|
||||
# 从字节码加载
|
||||
bytecode_blob = row["bytecode"]
|
||||
encrypted = row["encrypted"]
|
||||
@@ -80,3 +92,103 @@ class SQLiteModuleLoader(importlib.abc.Loader):
|
||||
module.__file__ = f"sqlite://{self.db_path}/{self.module_name}"
|
||||
|
||||
return module
|
||||
|
||||
def _exec_pyarmor_module(self, module, row):
|
||||
"""
|
||||
执行 PyArmor 保护的模块
|
||||
|
||||
PyArmor 混淆后的代码需要在运行时导入 pyarmor_runtime_xxx 包。
|
||||
该包包含 __init__.py 和原生 .pyd/.so 文件。
|
||||
我们需要从数据库提取这些文件并重建包结构到临时目录。
|
||||
"""
|
||||
source_code = row["source_code"]
|
||||
if not source_code:
|
||||
raise ImportError(
|
||||
f"PyArmor protected module '{self.module_name}' has no source code available"
|
||||
)
|
||||
|
||||
# 提取运行时模块名
|
||||
import re
|
||||
|
||||
match = re.search(
|
||||
r"from\s+(pyarmor_runtime_\w+)\s+import\s+__pyarmor__",
|
||||
source_code,
|
||||
)
|
||||
if not match:
|
||||
# 没有找到运行时引用,直接执行源代码
|
||||
bytecode = compile(
|
||||
source_code, f"<sqlite_module:{self.module_name}>", "exec"
|
||||
)
|
||||
exec(bytecode, module.__dict__)
|
||||
module.__file__ = f"sqlite://{self.db_path}/{self.module_name}"
|
||||
return module
|
||||
|
||||
runtime_name = match.group(1)
|
||||
|
||||
# 从数据库获取运行时包数据
|
||||
runtime_dict = None
|
||||
if self.db_path is not None:
|
||||
try:
|
||||
from .sqlite_module_importer import SQLiteModuleImporter
|
||||
|
||||
importer = SQLiteModuleImporter(self.db_path)
|
||||
runtime_dict = importer.get_runtime(runtime_name)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if runtime_dict:
|
||||
# 创建临时目录来存放运行时包
|
||||
self._runtime_temp_dir = tempfile.mkdtemp(prefix="pyarmor_runtime_")
|
||||
runtime_pkg_dir = os.path.join(self._runtime_temp_dir, runtime_name)
|
||||
os.makedirs(runtime_pkg_dir, exist_ok=True)
|
||||
|
||||
# 确定运行时二进制文件名(.pyd for Windows, .so for Linux/Mac)
|
||||
pyd_data = runtime_dict.get("pyd_data")
|
||||
if pyd_data:
|
||||
if sys.platform == "win32":
|
||||
so_filename = f"pyarmor_runtime.pyd"
|
||||
elif sys.platform == "darwin":
|
||||
so_filename = f"pyarmor_runtime.dylib"
|
||||
else:
|
||||
so_filename = f"pyarmor_runtime.so"
|
||||
|
||||
so_path = os.path.join(runtime_pkg_dir, so_filename)
|
||||
with open(so_path, "wb") as f:
|
||||
f.write(pyd_data)
|
||||
|
||||
# 写入 __init__.py
|
||||
init_data = runtime_dict.get("init_data")
|
||||
if init_data:
|
||||
init_path = os.path.join(runtime_pkg_dir, "__init__.py")
|
||||
with open(init_path, "w", encoding="utf-8") as f:
|
||||
f.write(init_data)
|
||||
|
||||
# 将临时目录添加到 sys.path,使运行时包可被导入
|
||||
if self._runtime_temp_dir not in sys.path:
|
||||
sys.path.insert(0, self._runtime_temp_dir)
|
||||
|
||||
# 设置 __file__ 属性(PyArmor 的 __pyarmor__() 需要它)
|
||||
module_file = f"sqlite://{self.db_path}/{self.module_name}"
|
||||
module.__file__ = module_file
|
||||
|
||||
# 如果是包,设置__path__和__package__属性
|
||||
if row["is_package"]:
|
||||
module.__path__ = []
|
||||
module.__package__ = self.module_name
|
||||
|
||||
# 执行混淆后的代码
|
||||
bytecode = compile(source_code, f"<sqlite_module:{self.module_name}>", "exec")
|
||||
exec(bytecode, module.__dict__)
|
||||
|
||||
return module
|
||||
|
||||
def cleanup_runtime(self):
|
||||
"""清理 PyArmor 运行时临时文件"""
|
||||
if self._runtime_temp_dir and os.path.exists(self._runtime_temp_dir):
|
||||
try:
|
||||
if self._runtime_temp_dir in sys.path:
|
||||
sys.path.remove(self._runtime_temp_dir)
|
||||
shutil.rmtree(self._runtime_temp_dir, ignore_errors=True)
|
||||
except Exception:
|
||||
pass
|
||||
self._runtime_temp_dir = None
|
||||
|
||||
@@ -891,11 +891,12 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "module-bank"
|
||||
version = "0.2.2"
|
||||
version = "0.3.0"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "cryptography", version = "45.0.7", source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }, marker = "python_full_version < '3.8'" },
|
||||
{ name = "cryptography", version = "46.0.3", source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }, marker = "python_full_version >= '3.8'" },
|
||||
{ name = "pyarmor" },
|
||||
]
|
||||
|
||||
[package.dev-dependencies]
|
||||
@@ -906,7 +907,10 @@ dev = [
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [{ name = "cryptography", specifier = ">=45.0.7" }]
|
||||
requires-dist = [
|
||||
{ name = "cryptography", specifier = ">=45.0.7" },
|
||||
{ name = "pyarmor", specifier = ">=9.2.4" },
|
||||
]
|
||||
|
||||
[package.metadata.requires-dev]
|
||||
dev = [{ name = "twine", specifier = ">=4.0.2" }]
|
||||
@@ -999,6 +1003,114 @@ wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/56/09/054aea9b7534a15ad38a363a2bd974c20646ab1582a387a95b8df1bfea1c/pkginfo-1.10.0-py3-none-any.whl", hash = "sha256:889a6da2ed7ffc58ab5b900d888ddce90bce912f2d2de1dc1c26f4cb9fe65097", size = 30392, upload-time = "2024-03-03T08:34:18.891Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyarmor"
|
||||
version = "9.2.4"
|
||||
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
|
||||
dependencies = [
|
||||
{ name = "pyarmor-cli-core" },
|
||||
]
|
||||
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/08/58/6f51b2a14fe14e9a98807cee85055ffba91b3991420a884f606bbdae6b76/pyarmor-9.2.4.tar.gz", hash = "sha256:09e9b6dddb65d287c07aa428933d8760821fa05614a25c30b5595222e777d60d", size = 3012328, upload-time = "2026-03-18T02:01:38.759Z" }
|
||||
wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/2f/d9/9d324a9acbe2daa37893895beeac201503d3f822196107185da16f21ed1b/pyarmor-9.2.4-py3-none-any.whl", hash = "sha256:2d7acea204739a5b390dcb9010b18ac747d646d1eac31d5f03d7b5670be11f8f", size = 3071639, upload-time = "2026-03-18T02:01:45.165Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyarmor-cli-core"
|
||||
version = "8.1.1"
|
||||
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
|
||||
wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/2a/26/3fd196462de9c66551cc03a5def656c1a27a64148421cc5528de3429b48f/pyarmor_cli_core-8.1.1-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:a5dc6923cc379a797d3265161cddc4db713577bdb3cfbb0f3663812cd945e4b6", size = 510402, upload-time = "2026-04-09T01:26:07.223Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/53/0d/2961d4ed2c8d582b1e078bda1e80689a5394955e06da915c26d222c56399/pyarmor_cli_core-8.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fe0c6782b4b0b26aa1bdc68e8f4cf772198c2ea548d4568d2334fcc30fa7f34b", size = 402418, upload-time = "2026-04-09T01:26:12.769Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/f4/a8/8c415f417299d7490151096ee78be4d97eef956e66a771b19a5161a93bb1/pyarmor_cli_core-8.1.1-cp310-cp310-manylinux1_i686.whl", hash = "sha256:943d584627fb48975b567f442a3ac1b71473582eec17606dd18240921d52b263", size = 481568, upload-time = "2026-04-09T01:26:17.196Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/54/ba/fd5acb8f325304ac918f14b765988622dbf526d5eed5c17b1f5b1f1fa64f/pyarmor_cli_core-8.1.1-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:f2a1777205bcc3737594e9fa111552ccd7877efc29ab2e2b13e4769f7eba3aff", size = 452540, upload-time = "2026-04-09T01:26:21.782Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/fb/92/cbc1184315524e6bb4226c962c3fc11326c500d1f333a734a9fbea8f724f/pyarmor_cli_core-8.1.1-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:d8f6d9d916cb83a1ac3256576158528d5bfb02404f324713ffe942d743a759d6", size = 598833, upload-time = "2026-04-09T01:26:26.116Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/03/d9/229e1fda8127e8558653b69e5245823aa16e770110897fceb712e621b41f/pyarmor_cli_core-8.1.1-cp310-cp310-manylinux2014_armv7l.whl", hash = "sha256:869ff6e51b8dd951620a22b5174d1d0795628f02c76df566d6790e7fbd68b6c9", size = 994953, upload-time = "2026-04-09T01:26:31.203Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/d4/87/9f1849c2e44c93dbd04f685f76aaaac4731800750451d52622389747de59/pyarmor_cli_core-8.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:c1d16c0069fec6daba35e384228e7d493ff7a83bd402bf3465527e2b8a1c1ad0", size = 643939, upload-time = "2026-04-09T01:26:36.522Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/3b/16/4f48172435e6b91a323da5b97cda8fa77620d5e934b7ae5eb90690ecf80a/pyarmor_cli_core-8.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:3dab4883b1ce7fcfa4212b94b7f5a538cc45a8a06742235caaf1b01f93b2b925", size = 439787, upload-time = "2026-04-09T01:26:41.738Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/4d/2e/96233f232008021b1977c3a356da55af287a7f36760a28d60f7061616a1b/pyarmor_cli_core-8.1.1-cp310-cp310-win32.whl", hash = "sha256:00cc0266c245d946bdaf2e0f85047a5a4813bee2f26971d45faa833e2bf67500", size = 454720, upload-time = "2026-04-09T01:26:46.266Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/52/42/fa668d204c631c03157f71865514b14d0d400b7804f2ba9c4858d1379bac/pyarmor_cli_core-8.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:07584b1251aadc9540d47747bed0193131411399501a9d42ae74c7555dfb6a9e", size = 414449, upload-time = "2026-04-09T01:26:51.043Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/22/e3/173a37ad0d0ded7302037965c0ee747260b939a95299e9ebf0898c1e14bb/pyarmor_cli_core-8.1.1-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:852fd36f9854dacafa20f26a375adf0b94160cbff68d6a3b6970f5a79e7d3629", size = 511673, upload-time = "2026-04-09T01:26:57.916Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/15/33/2c23d8dfb7b6a5dd39899f3d298826ca02cdc231746966196666bbe33384/pyarmor_cli_core-8.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a31b7ec9a79c2286898830365d7cc7e275af4e3cc7f99e1717e622c797165fbe", size = 403721, upload-time = "2026-04-09T01:27:02.812Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/f5/db/7aac19704813770f4acbcd6708d8afe181c21935fea1daed7621133a0fc6/pyarmor_cli_core-8.1.1-cp311-cp311-manylinux1_i686.whl", hash = "sha256:be3911a12c318b4cd93f7ffb492f3a8517c7e7c13b48dc53ea12988ef106ebb9", size = 484036, upload-time = "2026-04-09T01:27:07.129Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/cb/70/73a9b625901da86b2590f2fd874ea6c1efd8f5ff195763261ba7e6248253/pyarmor_cli_core-8.1.1-cp311-cp311-manylinux1_x86_64.whl", hash = "sha256:8fba7ea6f2884ef78df89018bc7ca67fd3aba989dc4d3e6d88fc5e069a88ef9b", size = 451883, upload-time = "2026-04-09T01:27:12.465Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/a6/dc/6afbee56cdfb8b1257bb9943782ce076c52bee546a532cb84541ec3179f5/pyarmor_cli_core-8.1.1-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:68d1e9bc7bc811a318965483dc079ce7da670799fc5ddb8ad1d3727216c437e3", size = 599055, upload-time = "2026-04-09T01:27:17.707Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/10/07/39a8cc9af7aabbe0d70ea62ec14d438bba5e88a69cc1be2f0dfa95bd8676/pyarmor_cli_core-8.1.1-cp311-cp311-manylinux2014_armv7l.whl", hash = "sha256:883c0ed2dfa19dab5a0c5edc8d8b4ad3b820491b1b4bbc4eaf5d4218a09caff5", size = 995541, upload-time = "2026-04-09T01:27:22.626Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/44/a4/efb2ebd451ee6bdda5c6ca359ab8a4bf7a0098cabc7d2d68d998ac9410b9/pyarmor_cli_core-8.1.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fedd378a0131d6df79e74e2cde4d039c7f19c14627301a7cba5a12ed200d55ab", size = 644557, upload-time = "2026-04-09T01:27:28Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/ff/65/cc33d6423712a36aa6b133e9bb9483a2936ee44c1a40e0b99658cff45f2a/pyarmor_cli_core-8.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3dd40308a8bd0ccd8a4e7c162fd8093de6a6e1fefb5ded2825c339e58f15a996", size = 441387, upload-time = "2026-04-09T01:27:33.25Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/ea/49/63509bc1b5d6bdb94306906a77638b4d4a2fdf6605244288990942b6ac50/pyarmor_cli_core-8.1.1-cp311-cp311-win32.whl", hash = "sha256:e5ce2e9442ff1a046cdd4a5c54c3df23640f37981877789420bf46d80943430c", size = 455036, upload-time = "2026-04-09T01:27:38.055Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/19/df/e0fd7f46bf412596d5cc3ffe6165e37d950b58aea4976bedc21e8c2a2419/pyarmor_cli_core-8.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d3f90b32d6b83c3bf73ff0f3d23f0045fc94671d6f467c9d50c641cea39795c", size = 414904, upload-time = "2026-04-09T01:27:42.651Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/58/b4/33fd5192cedc4600e9de8c9c7519205f421840da1b0df6e9491afca0a072/pyarmor_cli_core-8.1.1-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:4a6e09545fe74dc304992a0b002cc8d426c16ac3670d6f602d51838f080a2f31", size = 515480, upload-time = "2026-04-09T01:27:47.282Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/a6/e5/3310cb2e29bab80bd923d7e0749c2163550effe1d1a6c6d5d95f5c58d183/pyarmor_cli_core-8.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:df035854a6603d4861a50156484027a03c01a7b9385b5365cabea4871ac72e3e", size = 407703, upload-time = "2026-04-09T01:27:51.869Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/26/66/1aa494d360a05fd90677db8c4b8385dedd2083fe96fe6607ea555f800c87/pyarmor_cli_core-8.1.1-cp312-cp312-manylinux1_i686.whl", hash = "sha256:ec304457ff31b4939c8527640d7f525e78e9c91cc8307a11f068ef1291710b40", size = 489280, upload-time = "2026-04-09T01:27:56.492Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/ef/ad/5f246234ce778dd3b7bd4faa2bded71690bbcddbf8f29506cba131ae4f85/pyarmor_cli_core-8.1.1-cp312-cp312-manylinux1_x86_64.whl", hash = "sha256:9b65a62a71a88fb7534002067b972aac2f363e871ad34563579550fec1a260cb", size = 457685, upload-time = "2026-04-09T01:28:01.313Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/05/5a/cbaa2088483837ded4bb6301993de16743c633f230bf29947cdc10f7c307/pyarmor_cli_core-8.1.1-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:d1c4e623dbb8bd93fc4273a7ca79d192618ca36cf74aceaeca9d36c83608ed77", size = 604340, upload-time = "2026-04-09T01:28:06.227Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/87/39/2686a96a3d80c5ef61fea10066677ddf3678bdd48afcec04a0d268203fa0/pyarmor_cli_core-8.1.1-cp312-cp312-manylinux2014_armv7l.whl", hash = "sha256:0c10ad18ca2cb285b4b6c2bfdba6bc0234e84272f52bf692dce25ae7c6a21d56", size = 1001029, upload-time = "2026-04-09T01:28:11.426Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/77/e9/da903ea517387ac3d560b324fd03a7f50267cc2f68e99f84934fd9ee3ef9/pyarmor_cli_core-8.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:812201887aa2fb90f8b865a5dd443aa841cb951bd25d14dad843ce11b3ceef57", size = 649535, upload-time = "2026-04-09T01:28:16.373Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/91/eb/fcd742456b3a475e16eb5c3468132a9950095169cf1e6a64e4fb4bac208d/pyarmor_cli_core-8.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:88e129603512326ce1440e3aa105506304424ee40366cc19f0a72a9aeeabecf7", size = 446443, upload-time = "2026-04-09T01:28:21.054Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/b1/87/b58aed090a6a9debf65045f37e93b779e330c22106aa4cbb9aaaf7bb12b8/pyarmor_cli_core-8.1.1-cp312-cp312-win32.whl", hash = "sha256:b645d69c88f55a08d34fdd547759079ac1722e246d8e994ce3eb07937bdf318b", size = 460155, upload-time = "2026-04-09T01:28:25.665Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/ac/b8/1dc8b15bc1607a27ed78025d5f50c97c78542930cab15a6d6d457b664bb3/pyarmor_cli_core-8.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:09df045973b5de85ecb950f3fffc23686caa5b43749d4d966ccd150719639e2b", size = 420620, upload-time = "2026-04-09T01:28:30.36Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/2b/35/4b2969ebaea72869a5ba13847d47682ec77a1ff894fc83450e23e7257845/pyarmor_cli_core-8.1.1-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:86a576252f26752cfed5a1f0d6a12352f7ca8f38aadfd7e1df44453d933bf70c", size = 517354, upload-time = "2026-04-09T01:28:35.086Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/cc/0f/c688f09d158d5d4bf3c83289b9d16d454d5ac001ae962a7501d4209dbcad/pyarmor_cli_core-8.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ef4cd7ecf1aeca5748d221e548a6203f58ede46e3b1f31cc1fbba5170f1dc5c5", size = 409747, upload-time = "2026-04-09T01:28:40.159Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/f5/f1/c97c393fa0f6431ac6537acd21f117025488b1254c255ae42da1809d2ae3/pyarmor_cli_core-8.1.1-cp313-cp313-manylinux1_i686.whl", hash = "sha256:e95cfbaafee2eec3a3ef918c80f6ff6d6e26535c0b9f5ea615a97cf419a45603", size = 489761, upload-time = "2026-04-09T01:28:45.023Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/9c/78/5874a8cecc9123475bcf5884037a6abd5859e4b93e89bc5eacc06dfd493f/pyarmor_cli_core-8.1.1-cp313-cp313-manylinux1_x86_64.whl", hash = "sha256:2f42447c95f0de7df586a5899b395dcbf3193b5288a290f77dde90d4459700a6", size = 458105, upload-time = "2026-04-09T01:28:49.719Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/ff/a0/73eb8557f0dd6f9910dfb1ff70ad31550d5b037dff78f80a006732efb085/pyarmor_cli_core-8.1.1-cp313-cp313-manylinux2014_aarch64.whl", hash = "sha256:635144791ddcf3a7bbd31ac10181828d40636e4b6a477ffefa0c6f4073524604", size = 604874, upload-time = "2026-04-09T01:28:54.871Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/ce/6c/3eeab3e37748ae5354eb629ed5e03ee09c85c6abac8d77926d4d27507f52/pyarmor_cli_core-8.1.1-cp313-cp313-manylinux2014_armv7l.whl", hash = "sha256:248bfb04d86f38851bdc13652d5e447e0415128c0cdfbb04498802e624a88475", size = 1001331, upload-time = "2026-04-09T01:29:00.195Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/8f/f3/9736f5049e0fff5a333a8e14058e5b6b89ef68f469bcc23f9001fd47ab42/pyarmor_cli_core-8.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7a71b3b4ab8d07259b82c9fdad18e099d86b0880bce352949b834db825cbd697", size = 651229, upload-time = "2026-04-09T01:29:05.072Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/13/e5/988ebde164710de231a93127a1df25a278307bb8cd31e75b207266a3ae8b/pyarmor_cli_core-8.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:bc51089e37602a4e4c75b990448c44e25d7b1c7048805ad789863c889e80793f", size = 447196, upload-time = "2026-04-09T01:29:09.99Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/3a/5c/4be2fe6a77477c6a82b776c147487030978fb3c6c825dfcecafe4cdaf1d1/pyarmor_cli_core-8.1.1-cp313-cp313-win32.whl", hash = "sha256:689356f95fec04a53f91a624b0d1f4bfc8547e2ddede6799fb1c728377a51e19", size = 460848, upload-time = "2026-04-09T01:29:15.702Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/1b/d1/80a1085e94a8bcc3f1d8c6d1ef703c1fc35afe9a8f2b5f0676d74fc1a80d/pyarmor_cli_core-8.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:2e7b259c4dda700360030cf212b2cca625aaf9e7498dc9bfcbee9ef85acfec9b", size = 421343, upload-time = "2026-04-09T01:29:20.354Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/b4/57/12936c3ea69e88481e71d4f61a878aa49b26181c0fed9282b590f46b9132/pyarmor_cli_core-8.1.1-cp314-cp314-macosx_10_14_x86_64.whl", hash = "sha256:4bfbfe564881bd467b2103b47b4fdfe883746e39651545280f4c815d409e45f7", size = 522708, upload-time = "2026-04-09T01:29:25.471Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/fc/4c/ba326b377d0d698fa80864a478822e56acd0b1dd74db7b2645ca66e3c732/pyarmor_cli_core-8.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0ff3494acdacbec1565a1d98065ab2a2b47683778ad02b89b643008a1ffe90bd", size = 414399, upload-time = "2026-04-09T01:29:30.291Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/f5/15/f6121674cca8487665bc4803da745a1f4e7f98d233bf33e659ca3dbfec29/pyarmor_cli_core-8.1.1-cp314-cp314-manylinux1_i686.whl", hash = "sha256:2d905427cd23226f86998a28f6c79c849136dfb85e569e32b208ad7d75b24853", size = 495367, upload-time = "2026-04-09T01:29:35.223Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/02/6a/67121dc6f489120322bf4742774a3ebd05eb4b6d1ebcf971b34ea91ec2b4/pyarmor_cli_core-8.1.1-cp314-cp314-manylinux1_x86_64.whl", hash = "sha256:4de138aacd465e0a0d4601b290d0555d47657f59b86d5eeec1bc660fd01e149a", size = 462060, upload-time = "2026-04-09T01:29:39.799Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/bf/87/09d5c023474495dd3dbb789ecf538af1bcbe53c49cd07a703cfa5da03bde/pyarmor_cli_core-8.1.1-cp314-cp314-manylinux2014_aarch64.whl", hash = "sha256:1777c7c2c7130a34b7c7125ddd828569e3f938fc91efc0db99fb0ee1eba88aa4", size = 609493, upload-time = "2026-04-09T01:29:44.322Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/5c/55/c6f403c7a33900d1a194839e55677814a35ab33d10c4234394f7ad12c311/pyarmor_cli_core-8.1.1-cp314-cp314-manylinux2014_armv7l.whl", hash = "sha256:5172681e95bfbf4eb250d53e1ede75b270e7ec7c29a4accfb0acf766bc7b6351", size = 1004962, upload-time = "2026-04-09T01:29:49.616Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/bb/bc/ce7146830a08306884929b53f785bb708df7c360a73a2a3687ff73533918/pyarmor_cli_core-8.1.1-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:06de0a47cb8ae86fe9270f3930d8e245fff73f87bd3c49cdc2af47c8ce1a5b06", size = 655381, upload-time = "2026-04-09T01:29:54.994Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/aa/05/0fac5b9e9d37e1ea46ceae2733ab07d348b199257d99f69d290eae363ac0/pyarmor_cli_core-8.1.1-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:7ae1c265c6560e869d355373fd86c2880a951db6bbf2bbdffec698d47c269be9", size = 450987, upload-time = "2026-04-09T01:30:00.065Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/82/84/173c89753b13037ad45fd6c220aeb12e055de261c8453025df9dc0c82b2c/pyarmor_cli_core-8.1.1-cp314-cp314-win32.whl", hash = "sha256:18a7f8aa4054395be9fe5b051727f2c32bdce74d609ef75cb869a9fd0396da90", size = 465415, upload-time = "2026-04-09T01:30:04.793Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/38/ce/32fb60e0971791c7e102ab87140d8c5da444e8b22b05614d7b63b7a90113/pyarmor_cli_core-8.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:9e50864097734b039060074b4913ffca73069162ff3c32556f3b594888129661", size = 424573, upload-time = "2026-04-09T01:30:09.704Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/d2/02/7632897343d0b1ac3faa6e882f62880c4910d1331a46f1796816b99e9f8d/pyarmor_cli_core-8.1.1-cp315-cp315-macosx_10_14_x86_64.whl", hash = "sha256:eb3a63691a67aca10e2af7937baf0c88effc459c78c3c4ef1c48cf562376b830", size = 522819, upload-time = "2026-04-09T01:30:14.265Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/0b/aa/09cce8ed725419d8a3c1a4a4b588b5f369094ef0b9768bc7e2d7fe855897/pyarmor_cli_core-8.1.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:75328d808fb790546272ba92440c8ae9e80f3c84cce3f423fefb00a9af230af0", size = 414398, upload-time = "2026-04-09T01:30:18.603Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/14/a9/844a2fcb09dc30994e03b5469ed80d6066e794828cd48af437da33029f8d/pyarmor_cli_core-8.1.1-cp315-cp315-manylinux1_i686.whl", hash = "sha256:e0e0965e8a1d779485f747b93d6f4c7a20395553851921b0fd22ea89fb09287e", size = 495322, upload-time = "2026-04-09T01:30:23.59Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/4e/1b/b169c06628dc9ac375c5d3f270cd742e344ddfa6dab5e00216a984ccfa01/pyarmor_cli_core-8.1.1-cp315-cp315-manylinux1_x86_64.whl", hash = "sha256:3562142724c8cd48a6b161b35a7b4ebfc30820f08ca0adc28f8729077531c3f4", size = 461861, upload-time = "2026-04-09T01:30:28.655Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/bb/98/8ab28d635e03d269234243c4ac2fd82d722435640d9be89e78cb6f4ba15f/pyarmor_cli_core-8.1.1-cp315-cp315-manylinux2014_aarch64.whl", hash = "sha256:27c6e269131da709bcf1aac04ed8285eaa8a328f7682e98c4e93a81f97e2e4db", size = 609340, upload-time = "2026-04-09T01:30:33.652Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/f3/68/f6e381d81e988077c6fdddcfa659ed30953640e863928575886815e1eefa/pyarmor_cli_core-8.1.1-cp315-cp315-manylinux2014_armv7l.whl", hash = "sha256:0cc8d7c337663eb9d108649ba296973284ff8b6fc0f83ae0ce347697533a983d", size = 1004837, upload-time = "2026-04-09T01:30:38.944Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/cb/83/4958e7eb4a4dd27b024a034443636e7db418d06ee743eb09eecab2360e18/pyarmor_cli_core-8.1.1-cp315-cp315-musllinux_1_1_aarch64.whl", hash = "sha256:00cccda40da819c45550625c910f836b9c768699ea40b04c058c3221270b514b", size = 655276, upload-time = "2026-04-09T01:30:44.053Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/68/d8/c8992fc6f66fcd8b29f8497ca352165bd9e0972a1f5371eb8c01aaca7a3e/pyarmor_cli_core-8.1.1-cp315-cp315-musllinux_1_1_x86_64.whl", hash = "sha256:488be5b88a0c434fb17457972f6b7f681b38650ce078c3f19dced5b15091146d", size = 450926, upload-time = "2026-04-09T01:30:49.022Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/85/61/6cb081493c75662dd979a6cf8f6092d81605a9a59c2439a2ab77f3abfc1b/pyarmor_cli_core-8.1.1-cp315-cp315-win32.whl", hash = "sha256:a0f087fe8c388d178859dbc2a14e11e12bf8efc694f035e41d1dfc5b4a630052", size = 465352, upload-time = "2026-04-09T01:30:53.861Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/6e/87/9daf968e6e6add10d089eef86b46a26c9908bfae74820bc97aacbf37d081/pyarmor_cli_core-8.1.1-cp315-cp315-win_amd64.whl", hash = "sha256:c1b96471bb1d9fc575ba526841829c6bf03cd9db5caabf5688518a29ed389870", size = 424475, upload-time = "2026-04-09T01:30:58.457Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/c4/5c/e653770e60de273502616202ffcf769011a50c744442bd63e01506ee61d6/pyarmor_cli_core-8.1.1-cp37-cp37-macosx_10_14_x86_64.whl", hash = "sha256:6d41880b098558f3905a18ffa3c9498e95ec71d93ddb3bc2f23701947e174862", size = 509125, upload-time = "2026-04-09T01:31:03.266Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/ac/71/da506483c853b23eb45e63678da67919c113894649f6fd7fa08a602447ce/pyarmor_cli_core-8.1.1-cp37-cp37-manylinux1_i686.whl", hash = "sha256:05d9c9c54636b09fcbb37a95c97ccb1df9718f0cad88e03eb72f12bf18922092", size = 479615, upload-time = "2026-04-09T01:31:08.596Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/e0/fa/249684e417a9fcdd369672232901dfad7fe84b8a7ed0e3c12457ef6f108e/pyarmor_cli_core-8.1.1-cp37-cp37-manylinux1_x86_64.whl", hash = "sha256:245e837ab2543ce7e2eaa39a80c3ef093ff97a51ced18ddb9052512859f8db72", size = 449406, upload-time = "2026-04-09T01:31:13.597Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/59/06/c41929814430426aa4f9a24ef736b210e8b2482d7bcc84ca0fa64bd577c6/pyarmor_cli_core-8.1.1-cp37-cp37-manylinux2014_aarch64.whl", hash = "sha256:bf023d2f6c0cf69013eb80da97da87e8a67c0b2dd1e6d3375f02e0f195b3524f", size = 592931, upload-time = "2026-04-09T01:31:19.038Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/48/fe/74bc6a0d3a3f41696f4a944872bca0c73aa124ddef2b8af8168fb11337c1/pyarmor_cli_core-8.1.1-cp37-cp37-manylinux2014_armv7l.whl", hash = "sha256:2a66cfed9f7dd72043389ed9b933ae57ffb00d28a8195f4d6aa446a8eda0a787", size = 989241, upload-time = "2026-04-09T01:31:24.261Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/d3/f2/343507785aa9d742589a57006cecb5fe8f5552bd05c3076712efffeedd30/pyarmor_cli_core-8.1.1-cp37-cp37-musllinux_1_1_aarch64.whl", hash = "sha256:183555f51b42d3be95b7fa248800ddfec6bcdc7ea843cfb4982be2671df8c39f", size = 639618, upload-time = "2026-04-09T01:31:29.486Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/60/04/ae296dabb653962116cd9c7bb453b9d12fac8efa49bb67e2e98c15aec062/pyarmor_cli_core-8.1.1-cp37-cp37-musllinux_1_1_x86_64.whl", hash = "sha256:6575a73f3928782e3394bd4d74942fa60f926a3f78bcca5bb920b33e49647910", size = 435876, upload-time = "2026-04-09T01:31:34.288Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/f5/b5/f3647e1d1d7d58d12257d23bec0ee37838bc359a77b051fb724002283fc4/pyarmor_cli_core-8.1.1-cp37-cp37-win32.whl", hash = "sha256:6129c315fac01be1ceae09231816a42396d00928a96f537faf1ba3435263ee67", size = 447840, upload-time = "2026-04-09T01:31:39.095Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/5f/a0/b767e2303491a5028be332c5c38656e1e6953c224c11f51ef7ac98f0d3a9/pyarmor_cli_core-8.1.1-cp37-cp37-win_amd64.whl", hash = "sha256:84a8ad19400f52e75451c35a04e41d8034bbfdf8743a306346a800a995a5f470", size = 407502, upload-time = "2026-04-09T01:31:44.021Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/76/9c/dc11b51400a3c9ae97d092b169dbcbe5d3cdd7a48efa8253f173cec336a0/pyarmor_cli_core-8.1.1-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:db8b83e5d656ce2310a252ecb009f84ebeaf8c60be6a9b5fb6a7e08ac3dc0064", size = 510970, upload-time = "2026-04-09T01:31:48.658Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/0c/84/c2a1832659e83ced41974a64b3317d80c72126b38a61314419dc0c3f578d/pyarmor_cli_core-8.1.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:8b397fcd7febfcf692daa5641ee175377840304cbc0456929c84bf0eeefc634e", size = 483313, upload-time = "2026-04-09T01:31:53.644Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/08/0b/df3cd01216f01d6122447e7d69b8562973a27e9df15c2cf0840872e30763/pyarmor_cli_core-8.1.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:b487b9e93a5e63f3e1ff7da2af6ef0db79aa4a92234613d8e7c93ca8239f525a", size = 460867, upload-time = "2026-04-09T01:31:58.436Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/c3/33/800fbb62b8a7193ddba3445a117900782125ec678b086bc786c0ff4ff6c9/pyarmor_cli_core-8.1.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:0838f354a4c299bf6204a5be510b193c37aa4b4bd2ed2ebd23046b0b75a345db", size = 597415, upload-time = "2026-04-09T01:32:03.52Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/31/7e/927a29f880ec35eef6499470642c079417d1ef6caa4c887a05d982a1cf22/pyarmor_cli_core-8.1.1-cp38-cp38-manylinux2014_armv7l.whl", hash = "sha256:a2dd5be9b02b3b4cdc300423bdb38453e34bef7c5397d36981c35280d0ea13bd", size = 993141, upload-time = "2026-04-09T01:32:08.932Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/1a/fb/d33f4128745823235f3d6f2488f1161f43d9f855c730abd598978e97b7c0/pyarmor_cli_core-8.1.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:08bb1e1071cb4356ce85ef8e7089cf85853cd3204dfb146a00f9401bca80a3a1", size = 644146, upload-time = "2026-04-09T01:32:13.936Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/f2/ba/addbf681e1fcb006a9590b721beac1bbe663553dc151d3b146a5a51cabc0/pyarmor_cli_core-8.1.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:a15fc2e4af49ffca703391a7adf29f39c8975f09afafd2461ad808dc2aeaf215", size = 439049, upload-time = "2026-04-09T01:32:19.879Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/49/97/a92a86b584499d4d8dc3ccd94ca92048892242d2d6fce84ba27bf66344f3/pyarmor_cli_core-8.1.1-cp38-cp38-win32.whl", hash = "sha256:b685e5cdd68660dc6fbcf1502b79a93ccd7969a5d411837d3dc4fa8f266df81a", size = 452679, upload-time = "2026-04-09T01:32:24.782Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/84/fc/b933559c1fccacc063bc04cd88d7cc48c84a1fb707ec03fe0b106e34337e/pyarmor_cli_core-8.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:a83d4d571012bffe2196a1b65f56abf04ee57dd09aa8d063ced99b0cbc21c341", size = 413687, upload-time = "2026-04-09T01:32:29.485Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/50/48/07a43938c9fb667d1b487aedf4ef691914cb049c398eb355585ad7d691ad/pyarmor_cli_core-8.1.1-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:a7dee28cefefc8e2c183e8b56a12d4029b9517f4f860ec17bed3cca5f495be62", size = 510309, upload-time = "2026-04-09T01:32:34.533Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/13/ed/ce279cdd805d22c0f315eebe1c41d541e0e3bd5f58057297102912fc38de/pyarmor_cli_core-8.1.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d4eedb4151c13c8fa55a0c2050a1f06513a8099c78983a99f5176b962892bf85", size = 402366, upload-time = "2026-04-09T01:32:39.294Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/4b/9e/841f564ad60e0d9a0ba05c416f1220b95038fe643f08bc5871c932d3549d/pyarmor_cli_core-8.1.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:3122cb3b9049d8f71bc459abdd7baa3a588d49a7cb2d20aaaf9d01e8b16ea84b", size = 481480, upload-time = "2026-04-09T01:32:44.122Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/f5/bf/d019a15c35f2c06a971c4140c471249d4da523afb37aed70855d80bf9e12/pyarmor_cli_core-8.1.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:fa41bb83dd13fdac97f11703afabe241253906ed233c39d5f2248a64a17a5efc", size = 461013, upload-time = "2026-04-09T01:32:49.154Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/50/c2/8c4d7314e1fc5262fa47ff1ae61eec421261e1160329cd940f4f463a590e/pyarmor_cli_core-8.1.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:0817f5493745b0521f437ad26713bdf892a102e86f2e482b73f73fdd1432c605", size = 598571, upload-time = "2026-04-09T01:32:53.983Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/3f/19/21dd64c879c3c15a960393ffbce3b6c15c8e760e1d7d99a284f19aabfab1/pyarmor_cli_core-8.1.1-cp39-cp39-manylinux2014_armv7l.whl", hash = "sha256:ccf8db789066281475faedd5b06253b28bed3316a1bce5716e84166604b3ca10", size = 994367, upload-time = "2026-04-09T01:32:59.448Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/48/2e/1e2fcd7779f376af4c3a8ede0c9e9ae9981d6e735a8525eee566c9d80329/pyarmor_cli_core-8.1.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b98390156ab4226d77dcc4e7b6ea79ce82ffd553aaf12b402b965468d0c30a4e", size = 643749, upload-time = "2026-04-09T01:33:04.734Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/d3/35/4ee9c4315aef4f28f9cfb47a50d4e48bfec220d77f5e1e86a36d50302b33/pyarmor_cli_core-8.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:27d776ab86890c84381b3eed7c5906550f929d11d3ed63b4d981e0b4670a7f94", size = 439652, upload-time = "2026-04-09T01:33:09.45Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/51/0e/06f20cb41f14e703cf78b68e37a25f2fa163f87a82914404d9c916437a92/pyarmor_cli_core-8.1.1-cp39-cp39-win32.whl", hash = "sha256:5d7a3b90c559a29596db18d7f99342da1ba4634d526e08cbbe4743d4c77e3679", size = 454449, upload-time = "2026-04-09T01:33:14.388Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/7d/75/681dfabc50249cb38f452c0139006d6df9302dc5e32925972506f31ec1fa/pyarmor_cli_core-8.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:8dcd779236cb32cae03f2c71c469fbb945e45c6ef04e38a7ad39e412fe7a8df0", size = 414029, upload-time = "2026-04-09T01:33:19.46Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/bd/90/89c95f00699600a2c476b1c80e97df5ea1f09b995a8abfc0a8f118f0bf38/pyarmor_cli_core-8.1.1-py3-none-any.whl", hash = "sha256:06eb9771c7d1f0ad7c00884f13ea42edfbffab8b28ef718ce197c0b4c69d97d5", size = 5177, upload-time = "2026-04-09T01:33:22.702Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pycparser"
|
||||
version = "2.21"
|
||||
|
||||
Reference in New Issue
Block a user