modify: S3对象存储
This commit is contained in:
+90
-5
@@ -315,7 +315,91 @@ cd /app
|
||||
|
||||
---
|
||||
|
||||
## 5. 隔离性约束
|
||||
## 5. 文件传输:直连 S3/Minio 对象存储(不依赖网关)
|
||||
|
||||
进程插件可直接对接 S3/Minio 对象存储做文件传输——上传、下载、删除、预签名 URL
|
||||
均由**插件自己的配置**(`[workers.config.storage]`)驱动,**数据路径不经过网关**
|
||||
(预签名 URL 模式下网关甚至不参与数据传输:设备/浏览器拿到 URL 后直传/直取 Minio)。
|
||||
|
||||
### 5.1 配置
|
||||
|
||||
```toml
|
||||
# config/workers/<name>/config.toml
|
||||
[workers.config.storage]
|
||||
endpoint = "192.168.1.10:9000" # Minio 地址 host:port(不带协议前缀)
|
||||
access_key = "minioadmin"
|
||||
secret_key = "minioadmin"
|
||||
bucket = "chongming-files"
|
||||
secure = false # false=http(默认),true=https
|
||||
region = "" # 可选
|
||||
auto_create_bucket = true # 可选,默认 true:启动时确保 bucket 存在
|
||||
```
|
||||
|
||||
语义(`chongming_worker.storage.StorageClient.from_config`):
|
||||
|
||||
| 情形 | 行为 |
|
||||
| ---- | ---- |
|
||||
| 未配置 / `enabled = false` | 存储关闭(`self.storage = None`),端点自行降级 |
|
||||
| 配置段存在但缺必填键(endpoint/access_key/secret_key/bucket) | **启动硬失败**(ValueError),快速反馈配置错误 |
|
||||
| 连接失败 / bucket 不可用 | 告警降级为 None(不影响其他端点) |
|
||||
|
||||
### 5.2 SDK 用法
|
||||
|
||||
基类在 `_bootstrap` 阶段检测到 `config["storage"]` 段即自动初始化
|
||||
`self.storage`(无需任何额外代码);插件在 `on_init` 发布到 global_var 供路由读取:
|
||||
|
||||
```python
|
||||
class DemoPlugin(Plugin):
|
||||
async def on_init(self):
|
||||
if self.storage is not None: # 配置了 storage 段才初始化
|
||||
self.set_var("storage", self.storage)
|
||||
```
|
||||
|
||||
路由内使用(同步核心 + async 包装,RPC handler 直接 await):
|
||||
|
||||
```python
|
||||
from ..global_var import require
|
||||
|
||||
storage = require("storage")
|
||||
await storage.aput_bytes("dir/file.txt", data) # 上传字节
|
||||
await storage.aput_file("dir/file.txt", "/tmp/a.txt") # 上传本地文件
|
||||
data = await storage.aget_bytes("dir/file.txt") # 下载字节
|
||||
await storage.aget_file("dir/file.txt", "/tmp/b.txt") # 下载到本地文件
|
||||
url = await storage.apresigned_put_url("dir/file.txt") # 预签名 PUT(设备直传)
|
||||
url = await storage.apresigned_get_url("dir/file.txt") # 预签名 GET(设备直取)
|
||||
await storage.adelete("dir/file.txt") # 删除
|
||||
```
|
||||
|
||||
API 一览(`chongming_worker.storage.StorageClient`):
|
||||
|
||||
| 方法(async 同名加 `a` 前缀) | 说明 |
|
||||
| ---- | ---- |
|
||||
| `put_file(object_key, local_path, content_type=None)` | 上传本地文件(fput_object) |
|
||||
| `put_bytes(object_key, data, content_type=None)` | 上传内存字节 |
|
||||
| `get_file(object_key, local_path)` | 下载到本地文件 |
|
||||
| `get_bytes(object_key)` | 下载为字节 |
|
||||
| `delete(object_key)` | 删除对象 |
|
||||
| `presigned_put_url(object_key, expires=3600)` | 预签名 PUT URL(默认 1 小时) |
|
||||
| `presigned_get_url(object_key, expires=3600)` | 预签名 GET URL(默认 1 小时) |
|
||||
|
||||
### 5.3 参考示例
|
||||
|
||||
`workers/demo` 演示了 4 个文件传输端点(`config/workers/demo/` 下对应 TOML):
|
||||
|
||||
- `POST /demo/presign_upload` — filename → object_key + 预签名 PUT URL(设备直传)
|
||||
- `GET /demo/presign_download` — object_key → 预签名 GET URL(设备直取)
|
||||
- `POST /demo/upload_bytes` — base64 小文件经 Zenoh RPC 上传(大文件请用预签名直传)
|
||||
- `GET /demo/download_bytes` — base64 小文件经 Zenoh RPC 下载
|
||||
|
||||
配置后重启 worker 即可用;未配置时这些端点返回「storage 未配置」错误(降级演示)。
|
||||
|
||||
> 依赖:minio SDK 随 `chongming-worker` 一起安装(utils/worker pyproject 已声明),
|
||||
> 进程插件无需额外 `requirements.txt`。SDK 侧 `chongming_worker.storage` 不依赖网关包,
|
||||
> 隔离性约束与打包流程均不受影响。
|
||||
|
||||
---
|
||||
|
||||
## 6. 隔离性约束
|
||||
|
||||
进程插件**禁止 import `chongming`(网关包)**,只允许依赖:
|
||||
|
||||
@@ -331,7 +415,7 @@ cd /app
|
||||
|
||||
---
|
||||
|
||||
## 6. 进程内契约校验
|
||||
## 7. 进程内契约校验
|
||||
|
||||
Worker 进程启动时自读 TOML 并**在自己进程内**做三方一致性校验
|
||||
(`chongming_worker.contracts.validate_worker_contracts`),校验失败直接退出:
|
||||
@@ -346,7 +430,7 @@ Worker 进程启动时自读 TOML 并**在自己进程内**做三方一致性校
|
||||
|
||||
---
|
||||
|
||||
## 7. 常见问题排查
|
||||
## 8. 常见问题排查
|
||||
|
||||
| 现象 | 原因 / 处理 |
|
||||
| ---- | ----------- |
|
||||
@@ -356,11 +440,12 @@ Worker 进程启动时自读 TOML 并**在自己进程内**做三方一致性校
|
||||
| 路由没出现 | worker 是否就绪(日志「路由发布完成」);网关 RouteRegistry 是否订阅(启动流程 5.1);每 10s 自愈重发布,稍等或重启 worker |
|
||||
| 路由一直 404 | worker 是否已退出(优雅退出会摘除路由);进程被 SIGKILL 时描述符残留,重启 worker 后重发布自愈覆盖 |
|
||||
| 找不到插件配置 | `worker_serve` 须在项目根目录下运行(读 `config/workers/<name>/config.toml`,`--config` 可指定全局配置路径) |
|
||||
| storage.* 端点报「storage 未配置」 | 插件 config.toml 未配置 `[workers.config.storage]`(或 `enabled = false` / 连接失败降级)。补齐配置后重启 worker;启动日志可见「对象存储已就绪」或「storage 连接失败」告警 |
|
||||
| 容器内 import `workers.demo` 失败 | 确认在容器工作目录(zip 根)启动,`start.sh` 内含 `cd "$(dirname "$0")"`,worker_serve 会把 CWD 加入 sys.path |
|
||||
|
||||
---
|
||||
|
||||
## 8. 相关命令速查
|
||||
## 9. 相关命令速查
|
||||
|
||||
```bash
|
||||
uv run worker_serve --type <name> # 本地调试独立 Worker 进程
|
||||
@@ -373,7 +458,7 @@ uv run python test_process_mode_e2e.py # 端到端测试(先启动网
|
||||
|
||||
---
|
||||
|
||||
## 9. 参考
|
||||
## 10. 参考
|
||||
|
||||
- [主 README:进程插件与混合部署](../README.md)
|
||||
- [Worker 插件开发指南(inline,结构约定与装饰器细节)](../src/plugins/README.md)
|
||||
|
||||
@@ -31,6 +31,7 @@ import chongming_worker.contracts as con_mod # noqa: E402
|
||||
import chongming_worker.process as proc_mod # noqa: E402
|
||||
import chongming_worker.route_config as rc_mod # noqa: E402
|
||||
import chongming_worker.routes_publisher as pub_mod # noqa: E402
|
||||
import chongming_worker.storage as sto_mod # noqa: E402
|
||||
|
||||
check("worker SDK 导入(阻断 chongming 后成功)", True)
|
||||
|
||||
@@ -48,10 +49,11 @@ check(
|
||||
import workers.demo # noqa: E402,F401 触发 @rpc_method 注册
|
||||
|
||||
routes = con_mod.validate_worker_contracts("demo")
|
||||
subjects = [r["subject"] for r in routes]
|
||||
check(
|
||||
"进程内契约校验通过(TOML ↔ 注册表 ↔ 模型)",
|
||||
len(routes) == 1 and routes[0]["subject"] == "demo.hello",
|
||||
f"routes={[r['subject'] for r in routes]}",
|
||||
len(routes) == 5 and "demo.hello" in subjects,
|
||||
f"routes={subjects}",
|
||||
)
|
||||
|
||||
# ========== 4. 代码级双重保险:SDK 无对网关包的 import 语句(AST 精确检查) ==========
|
||||
@@ -76,7 +78,7 @@ def _imports_gateway(mod) -> list:
|
||||
return found
|
||||
|
||||
|
||||
for mod in (proc_mod, con_mod, pub_mod, rc_mod):
|
||||
for mod in (proc_mod, con_mod, pub_mod, rc_mod, sto_mod):
|
||||
bad = _imports_gateway(mod)
|
||||
if bad:
|
||||
check(f"{mod.__name__} import 了网关包: {bad}", False)
|
||||
|
||||
@@ -0,0 +1,319 @@
|
||||
"""
|
||||
StorageClient(S3/Minio)单元测试 — 桩替换 minio SDK,不依赖真实 Minio 服务
|
||||
|
||||
覆盖:
|
||||
- from_config 语义: 未配置 / enabled=false → None;缺必填键 → ValueError;
|
||||
auto_create_bucket 确保 bucket;auto_create_bucket=false 且 bucket 不存在 → 降级 None;
|
||||
连接失败 → 告警降级为 None
|
||||
- 上传/下载/删除/预签名 URL 委托给 minio SDK(桩校验参数与对象数据)
|
||||
- Plugin 基类集成: _bootstrap 按 config 的 storage 段初始化 self.storage;
|
||||
未配置时保持 None;cleanup 关闭并置 None
|
||||
|
||||
运行: uv run python test_worker_storage.py(也兼容 pytest 收集)
|
||||
"""
|
||||
import asyncio
|
||||
import sys
|
||||
import tempfile
|
||||
import types
|
||||
from pathlib import Path
|
||||
|
||||
SRC = Path(__file__).parent
|
||||
sys.path.insert(0, str(SRC))
|
||||
|
||||
import chongming_worker.storage as storage_mod # noqa: E402
|
||||
from chongming_worker.plugin import Plugin # noqa: E402
|
||||
from chongming_worker.storage import StorageClient # noqa: E402
|
||||
|
||||
failures = []
|
||||
|
||||
|
||||
def check(name, cond, detail=""):
|
||||
status = "PASS" if cond else "FAIL"
|
||||
print(f"[{status}] {name}" + (f" — {detail}" if detail else ""))
|
||||
if not cond:
|
||||
failures.append(name)
|
||||
|
||||
|
||||
class _WriteResult:
|
||||
def __init__(self, object_name):
|
||||
self.object_name = object_name
|
||||
|
||||
|
||||
class FakeMinio:
|
||||
"""最小 minio SDK 桩:记录构造参数与对象操作,bucket/连接行为可控。"""
|
||||
|
||||
instances = []
|
||||
# 类级默认行为(每次 from_config 新建实例时生效,测试按需改写)
|
||||
default_bucket_exists = True
|
||||
default_fail_connect = False
|
||||
|
||||
def __init__(self, endpoint, access_key=None, secret_key=None,
|
||||
secure=False, region=None, http_client=None):
|
||||
self.endpoint = endpoint
|
||||
self.access_key = access_key
|
||||
self.secret_key = secret_key
|
||||
self.secure = secure
|
||||
self.region = region
|
||||
self.http_client = http_client
|
||||
self.bucket_exists_ok = FakeMinio.default_bucket_exists
|
||||
self.fail_connect = FakeMinio.default_fail_connect
|
||||
self.created_buckets = []
|
||||
self.objects = {}
|
||||
FakeMinio.instances.append(self)
|
||||
|
||||
def bucket_exists(self, bucket):
|
||||
if self.fail_connect:
|
||||
raise ConnectionError("connection refused")
|
||||
return self.bucket_exists_ok
|
||||
|
||||
def make_bucket(self, bucket, location=None):
|
||||
self.created_buckets.append((bucket, location))
|
||||
|
||||
def put_object(self, bucket, key, data, length, content_type=None):
|
||||
self.objects[key] = data.read(length) if hasattr(data, "read") else data
|
||||
return _WriteResult(key)
|
||||
|
||||
def fput_object(self, bucket, key, path, content_type=None):
|
||||
self.objects[key] = Path(path).read_bytes()
|
||||
return _WriteResult(key)
|
||||
|
||||
def get_object(self, bucket, key):
|
||||
return _FakeResponse(self.objects[key])
|
||||
|
||||
def fget_object(self, bucket, key, path):
|
||||
Path(path).write_bytes(self.objects[key])
|
||||
|
||||
def remove_object(self, bucket, key):
|
||||
self.objects.pop(key, None)
|
||||
|
||||
def presigned_put_object(self, bucket, key, expires=None):
|
||||
return f"http://minio/put/{bucket}/{key}?e={expires}"
|
||||
|
||||
def presigned_get_object(self, bucket, key, expires=None):
|
||||
return f"http://minio/get/{bucket}/{key}?e={expires}"
|
||||
|
||||
|
||||
class _FakeResponse:
|
||||
def __init__(self, data):
|
||||
self._data = data
|
||||
self.closed = False
|
||||
|
||||
def read(self):
|
||||
return self._data
|
||||
|
||||
def close(self):
|
||||
self.closed = True
|
||||
|
||||
def release_conn(self):
|
||||
pass
|
||||
|
||||
|
||||
def patch_minio():
|
||||
"""把 sys.modules['minio'] 替换为桩(from minio import Minio 可解析)。"""
|
||||
fake_mod = types.ModuleType("minio")
|
||||
fake_mod.Minio = FakeMinio
|
||||
old = sys.modules.get("minio")
|
||||
sys.modules["minio"] = fake_mod
|
||||
FakeMinio.instances = []
|
||||
FakeMinio.default_bucket_exists = True
|
||||
FakeMinio.default_fail_connect = False
|
||||
return old
|
||||
|
||||
|
||||
def restore_minio(old):
|
||||
if old is None:
|
||||
sys.modules.pop("minio", None)
|
||||
else:
|
||||
sys.modules["minio"] = old
|
||||
|
||||
|
||||
def valid_conf(**overrides):
|
||||
conf = {
|
||||
"endpoint": "127.0.0.1:9000",
|
||||
"access_key": "ak",
|
||||
"secret_key": "sk",
|
||||
"bucket": "files",
|
||||
}
|
||||
conf.update(overrides)
|
||||
return conf
|
||||
|
||||
|
||||
# ========== 1. from_config 语义 ==========
|
||||
def test_from_config_disabled():
|
||||
check("from_config(None) → None", StorageClient.from_config(None) is None)
|
||||
check("from_config({}) → None", StorageClient.from_config({}) is None)
|
||||
check(
|
||||
"enabled=false → None",
|
||||
StorageClient.from_config(valid_conf(enabled=False)) is None,
|
||||
)
|
||||
|
||||
|
||||
def test_from_config_missing_keys():
|
||||
for key in ("endpoint", "access_key", "secret_key", "bucket"):
|
||||
conf = valid_conf()
|
||||
conf.pop(key)
|
||||
try:
|
||||
StorageClient.from_config(conf)
|
||||
check(f"缺必填键 {key} → ValueError", False)
|
||||
except ValueError as e:
|
||||
check(f"缺必填键 {key} → ValueError", key in str(e))
|
||||
|
||||
|
||||
def test_from_config_construct_and_bucket():
|
||||
old = patch_minio()
|
||||
try:
|
||||
# 构造参数正确 + bucket 已存在 → 直接可用
|
||||
storage = StorageClient.from_config(valid_conf())
|
||||
fake = FakeMinio.instances[-1]
|
||||
check(
|
||||
"构造参数传递正确",
|
||||
storage is not None
|
||||
and fake.endpoint == "127.0.0.1:9000"
|
||||
and fake.access_key == "ak"
|
||||
and fake.secret_key == "sk"
|
||||
and fake.secure is False
|
||||
and storage.bucket == "files"
|
||||
and storage.endpoint == "127.0.0.1:9000",
|
||||
)
|
||||
|
||||
# secure=true / region 透传
|
||||
storage = StorageClient.from_config(
|
||||
valid_conf(secure=True, region="cn-east")
|
||||
)
|
||||
fake = FakeMinio.instances[-1]
|
||||
check(
|
||||
"secure/region 透传",
|
||||
storage is not None
|
||||
and fake.secure is True
|
||||
and fake.region == "cn-east",
|
||||
)
|
||||
|
||||
# bucket 不存在 + auto_create_bucket(默认 true)→ make_bucket
|
||||
FakeMinio.default_bucket_exists = False
|
||||
storage = StorageClient.from_config(valid_conf())
|
||||
fake = FakeMinio.instances[-1]
|
||||
check(
|
||||
"bucket 不存在时自动创建",
|
||||
storage is not None and fake.created_buckets == [("files", None)],
|
||||
f"created_buckets={fake.created_buckets}",
|
||||
)
|
||||
|
||||
# bucket 不存在 + auto_create_bucket=false → 降级 None
|
||||
storage = StorageClient.from_config(valid_conf(auto_create_bucket=False))
|
||||
check("auto_create_bucket=false 且 bucket 缺失 → None", storage is None)
|
||||
|
||||
# 连接失败 → 降级 None(不抛错)
|
||||
FakeMinio.default_fail_connect = True
|
||||
storage = StorageClient.from_config(valid_conf())
|
||||
check("连接失败 → 降级 None", storage is None)
|
||||
finally:
|
||||
restore_minio(old)
|
||||
|
||||
|
||||
# ========== 2. 对象操作委托(同步 + async) ==========
|
||||
def test_object_operations():
|
||||
old = patch_minio()
|
||||
try:
|
||||
storage = StorageClient.from_config(valid_conf())
|
||||
fake = FakeMinio.instances[-1]
|
||||
|
||||
# 同步: put_bytes / get_bytes 往返
|
||||
key1 = storage.put_bytes("a/1.txt", b"hello")
|
||||
check("put_bytes 上传并返回 object_key", key1 == "a/1.txt" and fake.objects == {"a/1.txt": b"hello"})
|
||||
check("get_bytes 下载往返", storage.get_bytes("a/1.txt") == b"hello")
|
||||
|
||||
# 同步: put_file / get_file 往返
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
src = Path(tmp) / "src.bin"
|
||||
src.write_bytes(b"file-data")
|
||||
storage.put_file("a/2.bin", src)
|
||||
check("put_file 上传", fake.objects.get("a/2.bin") == b"file-data")
|
||||
dst = storage.get_file("a/2.bin", Path(tmp) / "dst.bin")
|
||||
check("get_file 下载", dst.read_bytes() == b"file-data")
|
||||
|
||||
# 预签名 URL
|
||||
put_url = storage.presigned_put_url("a/3.bin")
|
||||
get_url = storage.presigned_get_url("a/3.bin")
|
||||
check(
|
||||
"预签名 PUT/GET URL",
|
||||
"http://minio/put/files/a/3.bin" in put_url
|
||||
and "http://minio/get/files/a/3.bin" in get_url,
|
||||
f"put={put_url}, get={get_url}",
|
||||
)
|
||||
|
||||
# 删除
|
||||
storage.delete("a/1.txt")
|
||||
check("delete 删除对象", "a/1.txt" not in fake.objects)
|
||||
|
||||
# async 包装(to_thread → 同步核心)
|
||||
async def run():
|
||||
await storage.aput_bytes("a/async.bin", b"async-data")
|
||||
data = await storage.aget_bytes("a/async.bin")
|
||||
url = await storage.apresigned_put_url("a/async.bin")
|
||||
await storage.adelete("a/async.bin")
|
||||
return data, url, "a/async.bin" not in fake.objects
|
||||
|
||||
data, url, deleted = asyncio.run(run())
|
||||
check(
|
||||
"async 包装可用(aput/aget/apresign/adelete)",
|
||||
data == b"async-data" and "http://minio/put" in url and deleted,
|
||||
)
|
||||
finally:
|
||||
restore_minio(old)
|
||||
|
||||
|
||||
# ========== 3. Plugin 基类集成 ==========
|
||||
class _StoragePlugin(Plugin):
|
||||
type = "storage_test"
|
||||
db_name = None
|
||||
auto_bind = False
|
||||
|
||||
|
||||
def test_plugin_integration():
|
||||
old = patch_minio()
|
||||
try:
|
||||
async def run_with_storage():
|
||||
p = _StoragePlugin()
|
||||
p.config = {"storage": valid_conf()}
|
||||
await p._bootstrap()
|
||||
ok_init = p.storage is not None
|
||||
ok_bucket = p.storage.bucket == "files"
|
||||
await p.cleanup("worker/storage_test/x")
|
||||
ok_cleanup = p.storage is None
|
||||
return ok_init, ok_bucket, ok_cleanup
|
||||
|
||||
ok_init, ok_bucket, ok_cleanup = asyncio.run(run_with_storage())
|
||||
check("Plugin._bootstrap 按 storage 配置初始化 self.storage", ok_init and ok_bucket)
|
||||
check("Plugin.cleanup 关闭并置 None", ok_cleanup)
|
||||
finally:
|
||||
restore_minio(old)
|
||||
|
||||
async def run_without_storage():
|
||||
p = _StoragePlugin()
|
||||
p.config = {} # 无 storage 段
|
||||
await p._bootstrap()
|
||||
return p.storage
|
||||
|
||||
val = asyncio.run(run_without_storage())
|
||||
check("无 storage 配置 → self.storage 保持 None", val is None)
|
||||
|
||||
|
||||
def main():
|
||||
print("========== StorageClient(S3/Minio)单元测试 ==========")
|
||||
for fn in (
|
||||
test_from_config_disabled,
|
||||
test_from_config_missing_keys,
|
||||
test_from_config_construct_and_bucket,
|
||||
test_object_operations,
|
||||
test_plugin_integration,
|
||||
):
|
||||
fn()
|
||||
print("\n" + ("=" * 40))
|
||||
if failures:
|
||||
print(f"❌ {len(failures)} 项未通过: {failures}")
|
||||
sys.exit(1)
|
||||
print("✅ StorageClient 全部通过")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -485,10 +485,11 @@ source = { git = "http://124.71.68.6:3000/chongming/chongming-stream-manager.git
|
||||
|
||||
[[package]]
|
||||
name = "chongming-worker"
|
||||
version = "0.2.0"
|
||||
source = { git = "http://124.71.68.6:3000/chongming/chongming-utils-worker.git#6dbdce177b2e44f2d0fd84df51d49bd690a8926b" }
|
||||
version = "0.3.0"
|
||||
source = { git = "http://124.71.68.6:3000/chongming/chongming-utils-worker.git#c4a998084fc42572e121a27b84082579e66dd3d0" }
|
||||
dependencies = [
|
||||
{ name = "eclipse-zenoh" },
|
||||
{ name = "minio" },
|
||||
{ name = "pydantic" },
|
||||
{ name = "tomli" },
|
||||
]
|
||||
@@ -969,6 +970,22 @@ wheels = [
|
||||
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "minio"
|
||||
version = "7.2.20"
|
||||
source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" }
|
||||
dependencies = [
|
||||
{ name = "argon2-cffi" },
|
||||
{ name = "certifi" },
|
||||
{ name = "pycryptodome" },
|
||||
{ name = "typing-extensions" },
|
||||
{ name = "urllib3" },
|
||||
]
|
||||
sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/40/df/6dfc6540f96a74125a11653cce717603fd5b7d0001a8e847b3e54e72d238/minio-7.2.20.tar.gz", hash = "sha256:95898b7a023fbbfde375985aa77e2cd6a0762268db79cf886f002a9ea8e68598", size = 136113, upload-time = "2025-11-27T00:37:15.569Z" }
|
||||
wheels = [
|
||||
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/3e/9a/b697530a882588a84db616580f2ba5d1d515c815e11c30d219145afeec87/minio-7.2.20-py3-none-any.whl", hash = "sha256:eb33dd2fb80e04c3726a76b13241c6be3c4c46f8d81e1d58e757786f6501897e", size = 93751, upload-time = "2025-11-27T00:37:13.993Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "module-bank"
|
||||
version = "0.3.2"
|
||||
@@ -1209,6 +1226,36 @@ wheels = [
|
||||
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pycryptodome"
|
||||
version = "3.23.0"
|
||||
source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" }
|
||||
sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/8e/a6/8452177684d5e906854776276ddd34eca30d1b1e15aa1ee9cefc289a33f5/pycryptodome-3.23.0.tar.gz", hash = "sha256:447700a657182d60338bab09fdb27518f8856aecd80ae4c6bdddb67ff5da44ef", size = 4921276, upload-time = "2025-05-17T17:21:45.242Z" }
|
||||
wheels = [
|
||||
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/04/5d/bdb09489b63cd34a976cc9e2a8d938114f7a53a74d3dd4f125ffa49dce82/pycryptodome-3.23.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:0011f7f00cdb74879142011f95133274741778abba114ceca229adbf8e62c3e4", size = 2495152, upload-time = "2025-05-17T17:20:20.833Z" },
|
||||
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/a7/ce/7840250ed4cc0039c433cd41715536f926d6e86ce84e904068eb3244b6a6/pycryptodome-3.23.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:90460fc9e088ce095f9ee8356722d4f10f86e5be06e2354230a9880b9c549aae", size = 1639348, upload-time = "2025-05-17T17:20:23.171Z" },
|
||||
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/ee/f0/991da24c55c1f688d6a3b5a11940567353f74590734ee4a64294834ae472/pycryptodome-3.23.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4764e64b269fc83b00f682c47443c2e6e85b18273712b98aa43bcb77f8570477", size = 2184033, upload-time = "2025-05-17T17:20:25.424Z" },
|
||||
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/54/16/0e11882deddf00f68b68dd4e8e442ddc30641f31afeb2bc25588124ac8de/pycryptodome-3.23.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb8f24adb74984aa0e5d07a2368ad95276cf38051fe2dc6605cbcf482e04f2a7", size = 2270142, upload-time = "2025-05-17T17:20:27.808Z" },
|
||||
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/d5/fc/4347fea23a3f95ffb931f383ff28b3f7b1fe868739182cb76718c0da86a1/pycryptodome-3.23.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d97618c9c6684a97ef7637ba43bdf6663a2e2e77efe0f863cce97a76af396446", size = 2309384, upload-time = "2025-05-17T17:20:30.765Z" },
|
||||
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/6e/d9/c5261780b69ce66d8cfab25d2797bd6e82ba0241804694cd48be41add5eb/pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9a53a4fe5cb075075d515797d6ce2f56772ea7e6a1e5e4b96cf78a14bac3d265", size = 2183237, upload-time = "2025-05-17T17:20:33.736Z" },
|
||||
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/5a/6f/3af2ffedd5cfa08c631f89452c6648c4d779e7772dfc388c77c920ca6bbf/pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:763d1d74f56f031788e5d307029caef067febf890cd1f8bf61183ae142f1a77b", size = 2343898, upload-time = "2025-05-17T17:20:36.086Z" },
|
||||
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/9a/dc/9060d807039ee5de6e2f260f72f3d70ac213993a804f5e67e0a73a56dd2f/pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:954af0e2bd7cea83ce72243b14e4fb518b18f0c1649b576d114973e2073b273d", size = 2269197, upload-time = "2025-05-17T17:20:38.414Z" },
|
||||
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/f9/34/e6c8ca177cb29dcc4967fef73f5de445912f93bd0343c9c33c8e5bf8cde8/pycryptodome-3.23.0-cp313-cp313t-win32.whl", hash = "sha256:257bb3572c63ad8ba40b89f6fc9d63a2a628e9f9708d31ee26560925ebe0210a", size = 1768600, upload-time = "2025-05-17T17:20:40.688Z" },
|
||||
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/e4/1d/89756b8d7ff623ad0160f4539da571d1f594d21ee6d68be130a6eccb39a4/pycryptodome-3.23.0-cp313-cp313t-win_amd64.whl", hash = "sha256:6501790c5b62a29fcb227bd6b62012181d886a767ce9ed03b303d1f22eb5c625", size = 1799740, upload-time = "2025-05-17T17:20:42.413Z" },
|
||||
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/5d/61/35a64f0feaea9fd07f0d91209e7be91726eb48c0f1bfc6720647194071e4/pycryptodome-3.23.0-cp313-cp313t-win_arm64.whl", hash = "sha256:9a77627a330ab23ca43b48b130e202582e91cc69619947840ea4d2d1be21eb39", size = 1703685, upload-time = "2025-05-17T17:20:44.388Z" },
|
||||
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/db/6c/a1f71542c969912bb0e106f64f60a56cc1f0fabecf9396f45accbe63fa68/pycryptodome-3.23.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:187058ab80b3281b1de11c2e6842a357a1f71b42cb1e15bce373f3d238135c27", size = 2495627, upload-time = "2025-05-17T17:20:47.139Z" },
|
||||
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/6e/4e/a066527e079fc5002390c8acdd3aca431e6ea0a50ffd7201551175b47323/pycryptodome-3.23.0-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:cfb5cd445280c5b0a4e6187a7ce8de5a07b5f3f897f235caa11f1f435f182843", size = 1640362, upload-time = "2025-05-17T17:20:50.392Z" },
|
||||
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/50/52/adaf4c8c100a8c49d2bd058e5b551f73dfd8cb89eb4911e25a0c469b6b4e/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67bd81fcbe34f43ad9422ee8fd4843c8e7198dd88dd3d40e6de42ee65fbe1490", size = 2182625, upload-time = "2025-05-17T17:20:52.866Z" },
|
||||
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/5f/e9/a09476d436d0ff1402ac3867d933c61805ec2326c6ea557aeeac3825604e/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8987bd3307a39bc03df5c8e0e3d8be0c4c3518b7f044b0f4c15d1aa78f52575", size = 2268954, upload-time = "2025-05-17T17:20:55.027Z" },
|
||||
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/f9/c5/ffe6474e0c551d54cab931918127c46d70cab8f114e0c2b5a3c071c2f484/pycryptodome-3.23.0-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa0698f65e5b570426fc31b8162ed4603b0c2841cbb9088e2b01641e3065915b", size = 2308534, upload-time = "2025-05-17T17:20:57.279Z" },
|
||||
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/18/28/e199677fc15ecf43010f2463fde4c1a53015d1fe95fb03bca2890836603a/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:53ecbafc2b55353edcebd64bf5da94a2a2cdf5090a6915bcca6eca6cc452585a", size = 2181853, upload-time = "2025-05-17T17:20:59.322Z" },
|
||||
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/ce/ea/4fdb09f2165ce1365c9eaefef36625583371ee514db58dc9b65d3a255c4c/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:156df9667ad9f2ad26255926524e1c136d6664b741547deb0a86a9acf5ea631f", size = 2342465, upload-time = "2025-05-17T17:21:03.83Z" },
|
||||
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/22/82/6edc3fc42fe9284aead511394bac167693fb2b0e0395b28b8bedaa07ef04/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:dea827b4d55ee390dc89b2afe5927d4308a8b538ae91d9c6f7a5090f397af1aa", size = 2267414, upload-time = "2025-05-17T17:21:06.72Z" },
|
||||
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/59/fe/aae679b64363eb78326c7fdc9d06ec3de18bac68be4b612fc1fe8902693c/pycryptodome-3.23.0-cp37-abi3-win32.whl", hash = "sha256:507dbead45474b62b2bbe318eb1c4c8ee641077532067fec9c1aa82c31f84886", size = 1768484, upload-time = "2025-05-17T17:21:08.535Z" },
|
||||
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/54/2f/e97a1b8294db0daaa87012c24a7bb714147c7ade7656973fd6c736b484ff/pycryptodome-3.23.0-cp37-abi3-win_amd64.whl", hash = "sha256:c75b52aacc6c0c260f204cbdd834f76edc9fb0d8e0da9fbf8352ef58202564e2", size = 1799636, upload-time = "2025-05-17T17:21:10.393Z" },
|
||||
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/18/3d/f9441a0d798bf2b1e645adc3265e55706aead1255ccdad3856dbdcffec14/pycryptodome-3.23.0-cp37-abi3-win_arm64.whl", hash = "sha256:11eeeb6917903876f134b56ba11abe95c0b0fd5e3330def218083c7d98bbcb3c", size = 1703675, upload-time = "2025-05-17T17:21:13.146Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic"
|
||||
version = "2.13.4"
|
||||
|
||||
Reference in New Issue
Block a user