modify: 多worker模式插件扩缩容问题
This commit is contained in:
@@ -105,7 +105,6 @@ base = [
|
|||||||
"fastapi.middleware.cors",
|
"fastapi.middleware.cors",
|
||||||
"fastapi.middleware.gzip",
|
"fastapi.middleware.gzip",
|
||||||
"uvicorn",
|
"uvicorn",
|
||||||
"apscheduler",
|
|
||||||
"diskcache",
|
"diskcache",
|
||||||
"chongming_cache",
|
"chongming_cache",
|
||||||
"chongming_cache.backends",
|
"chongming_cache.backends",
|
||||||
|
|||||||
+10
-2
@@ -29,8 +29,8 @@ dependencies = [
|
|||||||
"uvloop",
|
"uvloop",
|
||||||
"chongming-mqtt",
|
"chongming-mqtt",
|
||||||
"httptools",
|
"httptools",
|
||||||
"chongming-cli>=0.1.0",
|
"chongming-cli",
|
||||||
"bytedance>=0.1.0",
|
"bytedance",
|
||||||
]
|
]
|
||||||
description = "基于 modules-bank 的 fastapi 打包脚手架"
|
description = "基于 modules-bank 的 fastapi 打包脚手架"
|
||||||
name = "chongming-zenoh"
|
name = "chongming-zenoh"
|
||||||
@@ -56,6 +56,14 @@ url = "http://124.71.68.6:3000/api/packages/chongming/pypi/simple/"
|
|||||||
[[tool.uv.index]]
|
[[tool.uv.index]]
|
||||||
url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple"
|
url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple"
|
||||||
|
|
||||||
|
[tool.uv.workspace]
|
||||||
|
members = [
|
||||||
|
"utils/cli",
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.uv.sources]
|
||||||
|
chongming-cli = { workspace = true }
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
gunicorn = "chongming:gunicorn_serve"
|
gunicorn = "chongming:gunicorn_serve"
|
||||||
serve = "chongming:serve"
|
serve = "chongming:serve"
|
||||||
|
|||||||
@@ -52,10 +52,18 @@ async def lifespan(app: FastAPI):
|
|||||||
# 4.1 全局副本数协调:把当前网关登记进在线列表,供按在线网关分摊 quota
|
# 4.1 全局副本数协调:把当前网关登记进在线列表,供按在线网关分摊 quota
|
||||||
manager.register_gateway()
|
manager.register_gateway()
|
||||||
|
|
||||||
|
# 4.2 宿主级 inline worker 领导选举(基于共享 cache 的分布式锁,key 按网关隔离):
|
||||||
|
# gunicorn 多 worker 下每实例仅一个进程托管 inline worker(避免 N×count 冗余),
|
||||||
|
# 其余进程只做 HTTP 并经 Zenoh liveliness 发现调用;多实例各选各的 leader。
|
||||||
|
is_inline_owner = manager.acquire_inline_owner()
|
||||||
|
app.state.inline_owner_locked = is_inline_owner
|
||||||
|
|
||||||
# 5. 插件加载(混合模式:inline 进程内加载;external 由独立进程运行,
|
# 5. 插件加载(混合模式:inline 进程内加载;external 由独立进程运行,
|
||||||
# 其路由经 Zenoh 动态注册——进程编排交给 Docker/K8s,网关不拉起进程)
|
# 其路由经 Zenoh 动态注册——进程编排交给 Docker/K8s,网关不拉起进程)
|
||||||
worker_tasks, custom_routers, inline_types, external_types = (
|
worker_tasks, custom_routers, inline_types, external_types = (
|
||||||
await lifespan_setup.load_workers(app, session, manager)
|
await lifespan_setup.load_workers(
|
||||||
|
app, session, manager, is_inline_owner=is_inline_owner
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
# 5.1 动态路由注册表常驻启动:订阅 chongming/routes/**,external 插件的
|
# 5.1 动态路由注册表常驻启动:订阅 chongming/routes/**,external 插件的
|
||||||
@@ -102,6 +110,10 @@ async def lifespan(app: FastAPI):
|
|||||||
# 2. 清理 inline workers(cleanup_workers 内部按 load_mode 过滤)
|
# 2. 清理 inline workers(cleanup_workers 内部按 load_mode 过滤)
|
||||||
await lifespan_setup.cleanup_workers(app)
|
await lifespan_setup.cleanup_workers(app)
|
||||||
|
|
||||||
|
# 2.1 释放 inline worker 所有权分布式锁(仅持锁进程,避免误删其他进程获得的新锁)
|
||||||
|
if getattr(app.state, "inline_owner_locked", False):
|
||||||
|
await manager.release_inline_owner()
|
||||||
|
|
||||||
# 3. 取消刷新任务
|
# 3. 取消刷新任务
|
||||||
refresh_task = getattr(app.state, "refresh_task", None)
|
refresh_task = getattr(app.state, "refresh_task", None)
|
||||||
if refresh_task:
|
if refresh_task:
|
||||||
@@ -111,11 +123,12 @@ async def lifespan(app: FastAPI):
|
|||||||
except asyncio.CancelledError:
|
except asyncio.CancelledError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# 4. 取消所有 worker 任务
|
# 4. 取消所有 worker 任务(含 self-heal 接管后拉起的任务)
|
||||||
for task in worker_tasks:
|
all_worker_tasks = list(worker_tasks) + manager.extra_worker_tasks()
|
||||||
|
for task in all_worker_tasks:
|
||||||
task.cancel()
|
task.cancel()
|
||||||
if worker_tasks:
|
if all_worker_tasks:
|
||||||
await asyncio.gather(*worker_tasks, return_exceptions=True)
|
await asyncio.gather(*all_worker_tasks, return_exceptions=True)
|
||||||
|
|
||||||
# 4.5 取消后台统计/审计任务
|
# 4.5 取消后台统计/审计任务
|
||||||
await call_stats.shutdown()
|
await call_stats.shutdown()
|
||||||
|
|||||||
@@ -390,7 +390,8 @@ def _discover_plugins() -> list[dict]:
|
|||||||
return plugins
|
return plugins
|
||||||
|
|
||||||
|
|
||||||
async def load_workers(app: FastAPI, session: zenoh.Session, manager: WorkerManager):
|
async def load_workers(app: FastAPI, session: zenoh.Session, manager: WorkerManager,
|
||||||
|
is_inline_owner: bool = True):
|
||||||
"""
|
"""
|
||||||
加载 inline 插件,启动对应的 worker 任务(混合模式)。
|
加载 inline 插件,启动对应的 worker 任务(混合模式)。
|
||||||
|
|
||||||
@@ -401,6 +402,11 @@ async def load_workers(app: FastAPI, session: zenoh.Session, manager: WorkerMana
|
|||||||
同时自动发现插件模块中暴露的 APIRouter(约定:模块顶层名为 `router` 的
|
同时自动发现插件模块中暴露的 APIRouter(约定:模块顶层名为 `router` 的
|
||||||
APIRouter 实例),收集后供 register_custom_routes() 统一注册。
|
APIRouter 实例),收集后供 register_custom_routes() 统一注册。
|
||||||
|
|
||||||
|
is_inline_owner: 本进程是否为宿主级 inline worker 持有者。gunicorn 多 worker
|
||||||
|
下仅持有者进程真正拉起 inline worker(避免 N×count 冗余副本);非持有者进程
|
||||||
|
仍 import 插件模块以便注册自定义路由/契约校验,但不 create_workers,worker 经
|
||||||
|
Zenoh liveliness 被跨进程发现。单进程(uvicorn serve)时为 True,行为不变。
|
||||||
|
|
||||||
返回: (worker_tasks, custom_routers, inline_types, external_types)
|
返回: (worker_tasks, custom_routers, inline_types, external_types)
|
||||||
"""
|
"""
|
||||||
workers_conf = _discover_plugins()
|
workers_conf = _discover_plugins()
|
||||||
@@ -410,7 +416,8 @@ async def load_workers(app: FastAPI, session: zenoh.Session, manager: WorkerMana
|
|||||||
external_types = sorted({wc["type"] for wc in external_plugins})
|
external_types = sorted({wc["type"] for wc in external_plugins})
|
||||||
logger.info(
|
logger.info(
|
||||||
f"发现 {len(workers_conf)} 个 worker 插件: "
|
f"发现 {len(workers_conf)} 个 worker 插件: "
|
||||||
f"inline={inline_types or '无'}, external={external_types or '无'}"
|
f"inline={inline_types or '无'}, external={external_types or '无'} "
|
||||||
|
f"(本进程 inline 持有者: {is_inline_owner})"
|
||||||
)
|
)
|
||||||
|
|
||||||
for wc in external_plugins:
|
for wc in external_plugins:
|
||||||
@@ -456,11 +463,33 @@ async def load_workers(app: FastAPI, session: zenoh.Session, manager: WorkerMana
|
|||||||
custom_routers.append(plugin_router)
|
custom_routers.append(plugin_router)
|
||||||
logger.info(f"发现插件自定义路由: {module_name}.router")
|
logger.info(f"发现插件自定义路由: {module_name}.router")
|
||||||
|
|
||||||
|
# 非持有者进程:不立即拉起 inline worker,但仍在下方(build starter 后)
|
||||||
|
# 注册自愈启动器;worker 经 liveliness 被本进程发现调用,@rpc_method 与
|
||||||
|
# 自定义路由/契约校验依赖的模块 import 已在上文完成不依赖拉起。
|
||||||
factory = getattr(module, factory_name)
|
factory = getattr(module, factory_name)
|
||||||
if inspect.iscoroutinefunction(factory):
|
|
||||||
tasks = await factory(session, worker_config, count, worker_manager=manager)
|
# 定义"拉起本类型 worker"的协程,供持有者崩溃后的自愈接管复用;
|
||||||
else:
|
# 用默认参数捕获循环变量,避免闭包迟到绑定。
|
||||||
tasks = factory(session, worker_config, count, worker_manager=manager)
|
def _make_starter(session=session, config=worker_config, cnt=count,
|
||||||
|
mgr=manager, fac=factory):
|
||||||
|
async def starter():
|
||||||
|
if inspect.iscoroutinefunction(fac):
|
||||||
|
return await fac(session, config, cnt, worker_manager=mgr)
|
||||||
|
return fac(session, config, cnt, worker_manager=mgr)
|
||||||
|
return starter
|
||||||
|
|
||||||
|
starter = _make_starter()
|
||||||
|
manager.register_worker_starter(worker_type, starter)
|
||||||
|
|
||||||
|
# 非持有者进程:不拉起 inline worker(启动器已注册,持有者崩溃时由 refresh 自愈接管)
|
||||||
|
if not is_inline_owner:
|
||||||
|
logger.info(
|
||||||
|
"本进程非 %s 的 inline 持有者,跳过创建副本(liveliness 发现即可调用)",
|
||||||
|
worker_type,
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
tasks = await starter()
|
||||||
worker_tasks.extend(tasks)
|
worker_tasks.extend(tasks)
|
||||||
logger.info(f"已加载 Worker 插件: {module_name}.{factory_name} (副本数: {count})")
|
logger.info(f"已加载 Worker 插件: {module_name}.{factory_name} (副本数: {count})")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -660,7 +689,15 @@ async def shutdown_mqtt(app: FastAPI):
|
|||||||
|
|
||||||
|
|
||||||
async def cleanup_workers(app: FastAPI):
|
async def cleanup_workers(app: FastAPI):
|
||||||
"""调用每个 inline worker 配置中定义的 cleanup 函数。"""
|
"""调用每个 inline worker 配置中定义的 cleanup 函数。
|
||||||
|
|
||||||
|
仅 inline worker 持有者进程执行:非持有者进程未初始化插件(未调用 create_workers,
|
||||||
|
on_init 未执行),cleanup/on_cleanup 无资源可清,且可能引用未创建的对象而误报错。
|
||||||
|
"""
|
||||||
|
# app.state.inline_owner_locked 恒为 bool(lifespan 4.2 设置),缺省按持有处理
|
||||||
|
if not getattr(app.state, "inline_owner_locked", True):
|
||||||
|
logger.info("本进程非 inline worker 持有者,跳过插件 cleanup")
|
||||||
|
return
|
||||||
workers_conf = _discover_plugins()
|
workers_conf = _discover_plugins()
|
||||||
|
|
||||||
for wc in workers_conf:
|
for wc in workers_conf:
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ import random
|
|||||||
import socket
|
import socket
|
||||||
import time
|
import time
|
||||||
import orjson
|
import orjson
|
||||||
from typing import Any, AsyncGenerator, Dict, List, Optional
|
from typing import Any, AsyncGenerator, Awaitable, Callable, Dict, List, Optional
|
||||||
import zenoh
|
import zenoh
|
||||||
from chongming_cache import Cache
|
from chongming_cache import Cache, DistributedLock
|
||||||
from .logger import get_logger
|
from .logger import get_logger
|
||||||
|
|
||||||
logger = get_logger("worker_manager")
|
logger = get_logger("worker_manager")
|
||||||
@@ -81,6 +81,13 @@ class WorkerManager:
|
|||||||
self._gateway_id: str = (
|
self._gateway_id: str = (
|
||||||
os.environ.get("CHONGMING_GATEWAY_ID") or socket.gethostname()
|
os.environ.get("CHONGMING_GATEWAY_ID") or socket.gethostname()
|
||||||
)
|
)
|
||||||
|
# inline worker 领导选举持有的分布式锁及持有标记(acquire_inline_owner 时设置)
|
||||||
|
self._inline_owner_lock: Optional[DistributedLock] = None
|
||||||
|
self._inline_owner: bool = False
|
||||||
|
# inline worker 自愈接管:类型 -> 启动协程(load_workers 注册);_started_extra
|
||||||
|
# 记录接管后拉起的任务,供关停时取消。
|
||||||
|
self._worker_starters: Dict[str, Callable[[], Awaitable[List[Any]]]] = {}
|
||||||
|
self._started_extra: List[Any] = []
|
||||||
|
|
||||||
# ── 全局副本数协调(chongming-cache 单一事实来源)────────────
|
# ── 全局副本数协调(chongming-cache 单一事实来源)────────────
|
||||||
|
|
||||||
@@ -154,6 +161,94 @@ class WorkerManager:
|
|||||||
gws = [[gw_id, ts] for gw_id, ts in gws if gw_id != self._gateway_id]
|
gws = [[gw_id, ts] for gw_id, ts in gws if gw_id != self._gateway_id]
|
||||||
self._write_gateways(gws)
|
self._write_gateways(gws)
|
||||||
|
|
||||||
|
# ── inline worker 领导选举(gunicorn 多 worker 兼容)────────────────
|
||||||
|
# gunicorn `-w N` 会 fork N 个网关进程,各自跑 lifespan 并拉起 inline worker,
|
||||||
|
# 副本数变成 N×count(副本数量超预期)。用基于共享 cache 的分布式锁做领导选举:
|
||||||
|
# 每个网关(gateway_id 隔离的实例/宿主)只有一个进程托管其 inline worker,其余
|
||||||
|
# 进程只做 HTTP、经 Zenoh liveliness 照常发现调用。
|
||||||
|
# 与 MQTT Bridge 同一把锁模式:经共享 cache 后端(redis/共享卷 diskcache)可跨
|
||||||
|
# 机器收敛;key 含 gateway_id,多实例各选各的 leader、互不抢占,不破坏分摊语义。
|
||||||
|
|
||||||
|
def acquire_inline_owner(self) -> bool:
|
||||||
|
"""尝试成为本网关 inline worker 持有者(cache 支撑的分布式锁)。
|
||||||
|
|
||||||
|
成功(全局唯一持有者)返回 True,并在后台续约租约;持锁进程崩溃后租约过期,
|
||||||
|
gunicorn 复活的进程可重新获得(failover)。未配置共享 cache 时单进程视为持有者。
|
||||||
|
"""
|
||||||
|
if self.cache is None:
|
||||||
|
self._inline_owner = True # 无共享 cache:单进程即持有者
|
||||||
|
return True
|
||||||
|
lock = DistributedLock(self.cache, f"inline_owner:{self._gateway_id}")
|
||||||
|
if not lock.acquire():
|
||||||
|
self._inline_owner = False
|
||||||
|
return False
|
||||||
|
lock.start()
|
||||||
|
self._inline_owner_lock = lock
|
||||||
|
self._inline_owner = True
|
||||||
|
logger.info(
|
||||||
|
"[LOCK] inline owner: 本进程获得持有权 (gw=%s)", self._gateway_id
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
|
||||||
|
async def release_inline_owner(self) -> None:
|
||||||
|
"""释放 inline worker 持有权分布式锁(受保护:仅仍由本进程持有时才删除)。"""
|
||||||
|
lock = self._inline_owner_lock
|
||||||
|
if lock is not None:
|
||||||
|
await lock.stop()
|
||||||
|
lock.release()
|
||||||
|
self._inline_owner_lock = None
|
||||||
|
self._inline_owner = False
|
||||||
|
|
||||||
|
def is_inline_owner(self) -> bool:
|
||||||
|
"""返回本进程是否持有 inline worker 所有权(用于管理/诊断)。"""
|
||||||
|
return bool(self._inline_owner)
|
||||||
|
|
||||||
|
def register_worker_starter(self, worker_type: str, starter) -> None:
|
||||||
|
"""为某 inline 类型注册一个"拉起 worker"的协程(load_workers 调用),
|
||||||
|
供持有者崩溃后的自愈接管使用。starter: async () -> List[Task]
|
||||||
|
"""
|
||||||
|
self._worker_starters[worker_type] = starter
|
||||||
|
|
||||||
|
def extra_worker_tasks(self) -> List[Any]:
|
||||||
|
"""返回因自愈接管而拉起的 worker 任务(供网关关停时取消)。"""
|
||||||
|
return list(self._started_extra)
|
||||||
|
|
||||||
|
async def _maybe_heal_inline(self) -> None:
|
||||||
|
"""持有者进程崩溃后的自愈接管。
|
||||||
|
|
||||||
|
若本进程非持有者,却存在「已配置 inline 类型但没有无线活副本」的情况,
|
||||||
|
说明原持有者已失效:尝试经分布式锁抢得领导权成为新持有者,并拉起这些 worker。
|
||||||
|
分布式锁保证同一时刻只有一个进程胜出,因此不会出现双写/分裂(split-brain)。
|
||||||
|
|
||||||
|
背景:gunicorn 仅复活死掉的 worker 槽位不足以 failover——槽位重启可能在旧锁
|
||||||
|
租约尚未过期时发生,新进程抢锁失败而照样跳过 create_workers。本函数随 refresh
|
||||||
|
每周期(~15s)重试,租约过期后即可接管,形成兜底。
|
||||||
|
"""
|
||||||
|
# 已是持有者:正常托管,不走接管路径(避免与常规池任务重复 init)
|
||||||
|
if self._inline_owner or not self._worker_starters:
|
||||||
|
return
|
||||||
|
missing = [
|
||||||
|
wtype
|
||||||
|
for wtype in self._worker_starters
|
||||||
|
if not self._local_workers.get(wtype)
|
||||||
|
and not self._workers_cache.get(wtype)
|
||||||
|
]
|
||||||
|
if not missing:
|
||||||
|
return
|
||||||
|
# 仍需保证同一时刻仅一个进程成为新持有者
|
||||||
|
if not self.acquire_inline_owner():
|
||||||
|
return
|
||||||
|
logger.warning(
|
||||||
|
"[heal] 检出持有者进程失效,本进程接管并拉起 inline workers: %s", missing
|
||||||
|
)
|
||||||
|
for wtype in missing:
|
||||||
|
try:
|
||||||
|
tasks = await self._worker_starters[wtype]()
|
||||||
|
self._started_extra.extend(tasks or [])
|
||||||
|
logger.info("[heal] 已拉起 %s (tasks=%d)", wtype, len(tasks or []))
|
||||||
|
except Exception as e: # noqa: BLE001
|
||||||
|
logger.error("[heal] 拉起 %s 失败: %s", wtype, e)
|
||||||
|
|
||||||
def resolved_count(self, worker_type: str, default: int) -> int:
|
def resolved_count(self, worker_type: str, default: int) -> int:
|
||||||
"""获取该类 worker 的全局目标总量。
|
"""获取该类 worker 的全局目标总量。
|
||||||
|
|
||||||
@@ -249,6 +344,11 @@ class WorkerManager:
|
|||||||
self.heartbeat_gateway()
|
self.heartbeat_gateway()
|
||||||
except Exception as e: # noqa: BLE001
|
except Exception as e: # noqa: BLE001
|
||||||
logger.error(f"网关心跳失败: {e}")
|
logger.error(f"网关心跳失败: {e}")
|
||||||
|
# 4. inline 持有者崩溃自愈接管(见 _maybe_heal_inline)
|
||||||
|
try:
|
||||||
|
await self._maybe_heal_inline()
|
||||||
|
except Exception as e: # noqa: BLE001
|
||||||
|
logger.error(f"inline 自愈检查失败: {e}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"刷新 worker 列表失败: {e}")
|
logger.error(f"刷新 worker 列表失败: {e}")
|
||||||
await asyncio.sleep(self._refresh_interval)
|
await asyncio.sleep(self._refresh_interval)
|
||||||
|
|||||||
@@ -0,0 +1,162 @@
|
|||||||
|
"""
|
||||||
|
inline worker 领导选举 + 自愈接管测试(gunicorn 多 worker 兼容)
|
||||||
|
|
||||||
|
基于共享 chongming-cache(DistributedLock)验证 WorkerManager 的领导选举语义:
|
||||||
|
1. 多"进程"各自 acquire_inline_owner:同一时刻只有一个持有者(无分裂)
|
||||||
|
2. 不同 gateway_id(多实例)key 隔离,各选各的 leader,互不抢占
|
||||||
|
3. 持有者在场且副本存活时不触发接管(不重复 init)
|
||||||
|
4. 持有者崩溃(副本消失)后,非持有者经 _maybe_heal_inline 抢锁接管并调用注册的
|
||||||
|
starter 拉起 worker
|
||||||
|
5. 接管后其它进程不再重复接管;再次崩溃可继续 failover
|
||||||
|
6. 关停释放锁;保护性 release 不误删他人锁
|
||||||
|
|
||||||
|
运行: uv run python test/test_inline_leader.py
|
||||||
|
"""
|
||||||
|
import asyncio
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
_HERE = Path(__file__).resolve().parent # chongming-zenoh/
|
||||||
|
_PROJ = _HERE
|
||||||
|
for _p in (_PROJ / "src", _PROJ / "utils" / "worker" / "src"):
|
||||||
|
sys.path.insert(0, str(_p))
|
||||||
|
|
||||||
|
from chongming_cache import Cache
|
||||||
|
from chongming.app.core.worker_manager import WorkerManager
|
||||||
|
|
||||||
|
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 DummySession:
|
||||||
|
"""占位 session:测试不涉及真实 Zenoh,_maybe_heal_inline 不触碰 session"""
|
||||||
|
|
||||||
|
|
||||||
|
async def try_heal(loser):
|
||||||
|
"""非持有者进程尝试接管"""
|
||||||
|
await loser._maybe_heal_inline()
|
||||||
|
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
tmp = tempfile.TemporaryDirectory()
|
||||||
|
c1 = Cache(backend="diskcache", cache_store_path=tmp.name)
|
||||||
|
c2 = Cache(backend="diskcache", cache_store_path=tmp.name + "_g2")
|
||||||
|
|
||||||
|
# ── 1. 领导选举:唯一持有者 ──────────────────────────────
|
||||||
|
m1 = WorkerManager(session=DummySession(), cache=c1)
|
||||||
|
m2 = WorkerManager(session=DummySession(), cache=c1)
|
||||||
|
a1 = m1.acquire_inline_owner()
|
||||||
|
a2 = m2.acquire_inline_owner()
|
||||||
|
check("1.1 同一时刻仅一进程获得持有权", a1 != a2, f"m1={a1}, m2={a2}")
|
||||||
|
owner, loser = (m1, m2) if a1 else (m2, m1)
|
||||||
|
check("1.2 持有者 is_inline_owner()=True", owner.is_inline_owner())
|
||||||
|
check("1.3 失权者 is_inline_owner()=False", not loser.is_inline_owner())
|
||||||
|
|
||||||
|
# ── 2. 释放后可再次竞选 ──────────────────────────────────
|
||||||
|
await owner.release_inline_owner()
|
||||||
|
check("2.1 持锁者释放后不再持有", not owner.is_inline_owner())
|
||||||
|
ra = m1.acquire_inline_owner()
|
||||||
|
rb = m2.acquire_inline_owner()
|
||||||
|
check("2.2 释放后可再次成功选举(唯一持有者)", ra != rb, f"m1={ra}, m2={rb}")
|
||||||
|
await m1.release_inline_owner()
|
||||||
|
await m2.release_inline_owner()
|
||||||
|
|
||||||
|
# ── 3. 不同 gateway_id 隔离(多实例各选各 leader) ────────
|
||||||
|
# gateway_id 默认取 hostname;同宿主多实例须显式设置 CHONGMING_GATEWAY_ID 区分,
|
||||||
|
# 这儿模拟两个不同实例(gwA / gwB),验证锁 key 隔离、互不抢占。
|
||||||
|
_orig_gw = os.environ.get("CHONGMING_GATEWAY_ID")
|
||||||
|
os.environ["CHONGMING_GATEWAY_ID"] = "gwA"
|
||||||
|
ga1 = WorkerManager(session=DummySession(), cache=c2)
|
||||||
|
ga2 = WorkerManager(session=DummySession(), cache=c2)
|
||||||
|
os.environ["CHONGMING_GATEWAY_ID"] = "gwB"
|
||||||
|
gb1 = WorkerManager(session=DummySession(), cache=c2)
|
||||||
|
if _orig_gw is None:
|
||||||
|
os.environ.pop("CHONGMING_GATEWAY_ID", None)
|
||||||
|
else:
|
||||||
|
os.environ["CHONGMING_GATEWAY_ID"] = _orig_gw
|
||||||
|
|
||||||
|
acc_a = ga1.acquire_inline_owner() or ga2.acquire_inline_owner()
|
||||||
|
acc_b = gb1.acquire_inline_owner()
|
||||||
|
check("3.1 不同 gateway 互不抢占(各自选到 leader)", acc_a and acc_b)
|
||||||
|
check("3.2 gateway 内部仍只有一个持有者",
|
||||||
|
ga1.is_inline_owner() != ga2.is_inline_owner())
|
||||||
|
await ga1.release_inline_owner()
|
||||||
|
await ga2.release_inline_owner()
|
||||||
|
await gb1.release_inline_owner()
|
||||||
|
|
||||||
|
# ── 4. 自愈接管 ──────────────────────────────────────────
|
||||||
|
ha = WorkerManager(session=DummySession(), cache=c1)
|
||||||
|
hb = WorkerManager(session=DummySession(), cache=c1)
|
||||||
|
got = hb.acquire_inline_owner()
|
||||||
|
check("4.1 初始 hb 持锁、ha 未持锁", got and not ha.is_inline_owner())
|
||||||
|
|
||||||
|
started = []
|
||||||
|
|
||||||
|
async def fake_starter():
|
||||||
|
started.append("doubao")
|
||||||
|
return []
|
||||||
|
|
||||||
|
# 与真实 load_workers 一致:所有进程都注册 starter
|
||||||
|
ha.register_worker_starter("doubao", fake_starter)
|
||||||
|
hb.register_worker_starter("doubao", fake_starter)
|
||||||
|
|
||||||
|
# 4.2 持有者在场、副本存活 → 不接管
|
||||||
|
hb._local_workers["doubao"] = ["worker/doubao/worker-0"]
|
||||||
|
ha._workers_cache["doubao"] = ["worker/doubao/worker-0"]
|
||||||
|
await try_heal(ha)
|
||||||
|
check("4.2 持有者在场且副本存活:不接管、不拉起", not ha.is_inline_owner())
|
||||||
|
|
||||||
|
# 4.3 持有者崩溃(模拟:锁租约过期 + 副本消失)→ ha 接管并拉起
|
||||||
|
await hb.release_inline_owner() # 模拟原持有者崩溃后锁租约过期
|
||||||
|
ha._workers_cache.clear()
|
||||||
|
hb._local_workers.clear()
|
||||||
|
await try_heal(ha)
|
||||||
|
check("4.3 副本消失后非持有者接管", ha.is_inline_owner())
|
||||||
|
check("4.4 接管时调用 starter 拉起 worker", "doubao" in started)
|
||||||
|
|
||||||
|
# 4.5 接管后其它进程不再重复接管/拉起
|
||||||
|
n_started = len(started)
|
||||||
|
# 模拟场景:新副本已能被"所有进程"看到
|
||||||
|
hb._workers_cache["doubao"] = ["worker/doubao/worker-0"]
|
||||||
|
await try_heal(hb)
|
||||||
|
check("4.5 接管后其它进程不再重复拉起",
|
||||||
|
len(started) == n_started or hb.is_inline_owner())
|
||||||
|
|
||||||
|
# ── 5. 再次崩溃可继续 failover ───────────────────────────
|
||||||
|
await ha.release_inline_owner() # 模拟 ha 崩溃后锁租约过期
|
||||||
|
ha._workers_cache.clear()
|
||||||
|
hb._workers_cache.clear()
|
||||||
|
n_started = len(started)
|
||||||
|
await try_heal(hb)
|
||||||
|
check("5.1 再次崩溃后 hb 接管", hb.is_inline_owner())
|
||||||
|
check("5.2 failover 期间新增一次拉起", len(started) > n_started)
|
||||||
|
|
||||||
|
# ── 6. 保护性释放:失权者 release 不误删他人锁 ───────────
|
||||||
|
# 当前持有者为 hb,由 ha(非持有者) 释放——锁 token 不匹配,不应删除
|
||||||
|
await ha.release_inline_owner()
|
||||||
|
check("6.1 非持锁者 release 不误删(hb 仍持有)", hb.is_inline_owner())
|
||||||
|
# 之后真正由持锁者安装
|
||||||
|
await hb.release_inline_owner()
|
||||||
|
check("6.2 真正持锁者释放后不再持有", not hb.is_inline_owner())
|
||||||
|
|
||||||
|
for c in (c1, c2):
|
||||||
|
c.close()
|
||||||
|
|
||||||
|
print()
|
||||||
|
if failures:
|
||||||
|
print(f"FAILED {len(failures)}: {failures}")
|
||||||
|
return 1
|
||||||
|
print("ALL PASS")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(asyncio.run(main()))
|
||||||
@@ -6,6 +6,12 @@ resolution-markers = [
|
|||||||
"python_full_version < '3.14'",
|
"python_full_version < '3.14'",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[manifest]
|
||||||
|
members = [
|
||||||
|
"chongming-cli",
|
||||||
|
"chongming-zenoh",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aiofiles"
|
name = "aiofiles"
|
||||||
version = "25.1.0"
|
version = "25.1.0"
|
||||||
@@ -445,18 +451,30 @@ wheels = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "chongming-cli"
|
name = "chongming-cli"
|
||||||
version = "0.1.2"
|
version = "0.1.2"
|
||||||
source = { registry = "http://124.71.68.6:3000/api/packages/chongming/pypi/simple/" }
|
source = { editable = "utils/cli" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "chongming-config" },
|
{ name = "chongming-config" },
|
||||||
{ name = "module-bank" },
|
{ name = "module-bank" },
|
||||||
{ name = "sqlite-vfs" },
|
{ name = "sqlite-vfs" },
|
||||||
{ name = "tomli" },
|
{ name = "tomli" },
|
||||||
]
|
]
|
||||||
sdist = { url = "http://124.71.68.6:3000/api/packages/chongming/pypi/files/chongming-cli/0.1.2/chongming_cli-0.1.2.tar.gz", hash = "sha256:e169b81cb8c5dc6198c685b963b862f7cd27a2ac273da789fc23cbe027323265" }
|
|
||||||
wheels = [
|
[package.dev-dependencies]
|
||||||
{ url = "http://124.71.68.6:3000/api/packages/chongming/pypi/files/chongming-cli/0.1.2/chongming_cli-0.1.2-py3-none-any.whl", hash = "sha256:f72edb901ba1b355e2937f7fb1e9a9da4144c328bba59ea97421b5041b565e67" },
|
dev = [
|
||||||
|
{ name = "twine" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[package.metadata]
|
||||||
|
requires-dist = [
|
||||||
|
{ name = "chongming-config" },
|
||||||
|
{ name = "module-bank" },
|
||||||
|
{ name = "sqlite-vfs" },
|
||||||
|
{ name = "tomli" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.metadata.requires-dev]
|
||||||
|
dev = [{ name = "twine", specifier = ">=7.0.0" }]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "chongming-config"
|
name = "chongming-config"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@@ -602,9 +620,9 @@ dev = [
|
|||||||
requires-dist = [
|
requires-dist = [
|
||||||
{ name = "aiofiles" },
|
{ name = "aiofiles" },
|
||||||
{ name = "amqtt" },
|
{ name = "amqtt" },
|
||||||
{ name = "bytedance", specifier = ">=0.1.0" },
|
{ name = "bytedance" },
|
||||||
{ name = "chongming-cache" },
|
{ name = "chongming-cache" },
|
||||||
{ name = "chongming-cli", specifier = ">=0.1.0" },
|
{ name = "chongming-cli", editable = "utils/cli" },
|
||||||
{ name = "chongming-config" },
|
{ name = "chongming-config" },
|
||||||
{ name = "chongming-database" },
|
{ name = "chongming-database" },
|
||||||
{ name = "chongming-jwt" },
|
{ name = "chongming-jwt" },
|
||||||
@@ -631,7 +649,7 @@ requires-dist = [
|
|||||||
|
|
||||||
[package.metadata.requires-dev]
|
[package.metadata.requires-dev]
|
||||||
dev = [
|
dev = [
|
||||||
{ name = "chongming-cli" },
|
{ name = "chongming-cli", editable = "utils/cli" },
|
||||||
{ name = "nuitka" },
|
{ name = "nuitka" },
|
||||||
{ name = "pyarmor" },
|
{ name = "pyarmor" },
|
||||||
{ name = "pyinstaller" },
|
{ name = "pyinstaller" },
|
||||||
@@ -724,6 +742,15 @@ wheels = [
|
|||||||
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/3f/27/4570e78fc0bf5ea0ca45eb1de3818a23787af9b390c0b0a0033a1b8236f9/diskcache-5.6.3-py3-none-any.whl", hash = "sha256:5e31b2d5fbad117cc363ebaf6b689474db18a1f6438bc82358b024abd4c2ca19" },
|
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/3f/27/4570e78fc0bf5ea0ca45eb1de3818a23787af9b390c0b0a0033a1b8236f9/diskcache-5.6.3-py3-none-any.whl", hash = "sha256:5e31b2d5fbad117cc363ebaf6b689474db18a1f6438bc82358b024abd4c2ca19" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "docutils"
|
||||||
|
version = "0.23"
|
||||||
|
source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" }
|
||||||
|
sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/39/a4/5180d9afc57e8fca05601dd652bdff19604c218814037fe90ffc7625a50a/docutils-0.23.tar.gz", hash = "sha256:746f5060322511280a1e50eb76846ed6bf2342984b2ac04dc42caa1a8d78799e", size = 2303823, upload-time = "2026-05-27T17:41:06.934Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/32/91/30151a39f7570f448ed84529390628a651d7f27c87d73c9b887f8189695e/docutils-0.23-py3-none-any.whl", hash = "sha256:25d013af9bf23bc1c7b2b093dff4208166c53a94786c9e447808335ef1185fea", size = 634701, upload-time = "2026-05-27T17:40:58.442Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "eclipse-zenoh"
|
name = "eclipse-zenoh"
|
||||||
version = "1.10.0"
|
version = "1.10.0"
|
||||||
@@ -932,6 +959,18 @@ wheels = [
|
|||||||
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/9b/43/832f631d32e4f1211caa2ba368317739fe71f0b8530e4c9d15dc454bac2a/httpx2_jsfetch-1.0-py3-none-any.whl", hash = "sha256:cb916b707601e69a07721aabc8f3f6659be3a6893bc1ff5c6f9e02241df2da32", size = 6382, upload-time = "2026-08-07T00:13:06.567Z" },
|
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/9b/43/832f631d32e4f1211caa2ba368317739fe71f0b8530e4c9d15dc454bac2a/httpx2_jsfetch-1.0-py3-none-any.whl", hash = "sha256:cb916b707601e69a07721aabc8f3f6659be3a6893bc1ff5c6f9e02241df2da32", size = 6382, upload-time = "2026-08-07T00:13:06.567Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "id"
|
||||||
|
version = "1.6.1"
|
||||||
|
source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "urllib3" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/6d/04/c2156091427636080787aac190019dc64096e56a23b7364d3c1764ee3a06/id-1.6.1.tar.gz", hash = "sha256:d0732d624fb46fd4e7bc4e5152f00214450953b9e772c182c1c22964def1a069", size = 18088, upload-time = "2026-02-04T16:19:41.26Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/42/77/de194443bf38daed9452139e960c632b0ef9f9a5dd9ce605fdf18ca9f1b1/id-1.6.1-py3-none-any.whl", hash = "sha256:f5ec41ed2629a508f5d0988eda142e190c9c6da971100612c4de9ad9f9b237ca", size = 14689, upload-time = "2026-02-04T16:19:40.051Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "idna"
|
name = "idna"
|
||||||
version = "3.19"
|
version = "3.19"
|
||||||
@@ -941,6 +980,48 @@ wheels = [
|
|||||||
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/57/b0/0e52c878c53f245edd3a11020f20979b3f490f245af532c7cae3027754b5/idna-3.19-py3-none-any.whl", hash = "sha256:815e7be7a7806d54abb586dc943addc79e8b2ee16915059658cbeff4b1b43bf4", size = 68550, upload-time = "2026-08-18T05:14:22.343Z" },
|
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/57/b0/0e52c878c53f245edd3a11020f20979b3f490f245af532c7cae3027754b5/idna-3.19-py3-none-any.whl", hash = "sha256:815e7be7a7806d54abb586dc943addc79e8b2ee16915059658cbeff4b1b43bf4", size = 68550, upload-time = "2026-08-18T05:14:22.343Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jaraco-classes"
|
||||||
|
version = "3.4.0"
|
||||||
|
source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "more-itertools" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/06/c0/ed4a27bc5571b99e3cff68f8a9fa5b56ff7df1c2251cc715a652ddd26402/jaraco.classes-3.4.0.tar.gz", hash = "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd", size = 11780, upload-time = "2024-03-31T07:27:36.643Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl", hash = "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790", size = 6777, upload-time = "2024-03-31T07:27:34.792Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jaraco-context"
|
||||||
|
version = "6.1.2"
|
||||||
|
source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" }
|
||||||
|
sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/af/50/4763cd07e722bb6285316d390a164bc7e479db9d90daa769f22578f698b4/jaraco_context-6.1.2.tar.gz", hash = "sha256:f1a6c9d391e661cc5b8d39861ff077a7dc24dc23833ccee564b234b81c82dfe3", size = 16801, upload-time = "2026-03-20T22:13:33.922Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/f2/58/bc8954bda5fcda97bd7c19be11b85f91973d67a706ed4a3aec33e7de22db/jaraco_context-6.1.2-py3-none-any.whl", hash = "sha256:bf8150b79a2d5d91ae48629d8b427a8f7ba0e1097dd6202a9059f29a36379535", size = 7871, upload-time = "2026-03-20T22:13:32.808Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jaraco-functools"
|
||||||
|
version = "4.6.0"
|
||||||
|
source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "more-itertools" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/6c/1f/c23395957d41ccf27c4e535c3d334c4051e5395b3752057ba4cbaec35c56/jaraco_functools-4.6.0.tar.gz", hash = "sha256:880c577ec9720b3a052d5bc611fb9f2269b3d87902ef42440df443b88e443280", size = 20837, upload-time = "2026-07-14T01:28:02.544Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/02/36/ecc85bc96c273dc8a11273ed4782272975e6338d4a3e9228621175edf0e3/jaraco_functools-4.6.0-py3-none-any.whl", hash = "sha256:99e3dc0060c5cbe8fcd1cdb36258e2a65ca40f1566b2033b12abb1bb44dd3c30", size = 11677, upload-time = "2026-07-14T01:28:01.59Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jeepney"
|
||||||
|
version = "0.9.0"
|
||||||
|
source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" }
|
||||||
|
sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/7b/6f/357efd7602486741aa73ffc0617fb310a29b588ed0fd69c2399acbb85b0c/jeepney-0.9.0.tar.gz", hash = "sha256:cf0e9e845622b81e4a28df94c40345400256ec608d0e55bb8a3feaa9163f5732", size = 106758, upload-time = "2025-02-27T18:51:01.684Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl", hash = "sha256:97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683", size = 49010, upload-time = "2025-02-27T18:51:00.104Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "jiter"
|
name = "jiter"
|
||||||
version = "0.16.0"
|
version = "0.16.0"
|
||||||
@@ -1022,6 +1103,23 @@ wheels = [
|
|||||||
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/9e/54/b819eb2f8caedb1bccef0bf6188c051f0d898a177b271e1ed3a5cc0a8b66/jwcrypto-1.5.9-py3-none-any.whl", hash = "sha256:30b1c9afc898eefd112c31d836eb2d7a4c6febcac95d4aa727e963cd42911d8e", size = 120772, upload-time = "2026-08-26T12:54:08.637Z" },
|
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/9e/54/b819eb2f8caedb1bccef0bf6188c051f0d898a177b271e1ed3a5cc0a8b66/jwcrypto-1.5.9-py3-none-any.whl", hash = "sha256:30b1c9afc898eefd112c31d836eb2d7a4c6febcac95d4aa727e963cd42911d8e", size = 120772, upload-time = "2026-08-26T12:54:08.637Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "keyring"
|
||||||
|
version = "25.7.0"
|
||||||
|
source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "jaraco-classes" },
|
||||||
|
{ name = "jaraco-context" },
|
||||||
|
{ name = "jaraco-functools" },
|
||||||
|
{ name = "jeepney", marker = "sys_platform == 'linux'" },
|
||||||
|
{ name = "pywin32-ctypes", marker = "sys_platform == 'win32'" },
|
||||||
|
{ name = "secretstorage", marker = "sys_platform == 'linux'" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/43/4b/674af6ef2f97d56f0ab5153bf0bfa28ccb6c3ed4d1babf4305449668807b/keyring-25.7.0.tar.gz", hash = "sha256:fe01bd85eb3f8fb3dd0405defdeac9a5b4f6f0439edbb3149577f244a2e8245b", size = 63516, upload-time = "2025-11-16T16:26:09.482Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/81/db/e655086b7f3a705df045bf0933bdd9c2f79bb3c97bfef1384598bb79a217/keyring-25.7.0-py3-none-any.whl", hash = "sha256:be4a0b195f149690c166e850609a477c532ddbfbaed96a404d4e43f8d5e2689f", size = 39160, upload-time = "2025-11-16T16:26:08.402Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "loguru"
|
name = "loguru"
|
||||||
version = "0.7.3"
|
version = "0.7.3"
|
||||||
@@ -1096,6 +1194,49 @@ wheels = [
|
|||||||
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/8a/d3/0a7a8297ec3041e0352f313e020276a04c59bc9200138094df2a6d374f51/module_bank-0.3.2-py3-none-any.whl", hash = "sha256:524faaa86258ebbdc07df493a041566adfbdf57461487a63bf7161e6aa79a817", size = 19051, upload-time = "2026-05-04T16:21:19.827Z" },
|
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/8a/d3/0a7a8297ec3041e0352f313e020276a04c59bc9200138094df2a6d374f51/module_bank-0.3.2-py3-none-any.whl", hash = "sha256:524faaa86258ebbdc07df493a041566adfbdf57461487a63bf7161e6aa79a817", size = 19051, upload-time = "2026-05-04T16:21:19.827Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "more-itertools"
|
||||||
|
version = "11.1.0"
|
||||||
|
source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" }
|
||||||
|
sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/de/1d/f4da6f02cdffe04d6362210b807146a26044c88d839208aec273bb0d9184/more_itertools-11.1.0.tar.gz", hash = "sha256:48e8f4d9e7e5878571ecf6f2b4e57634f93cd474cc8cfbd2376f2d11b396e30d", size = 145772, upload-time = "2026-05-22T14:14:29.909Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/e8/3d/1087453384dbde46a8c7f9356eead2c58be8a7bf156bca40243377c85715/more_itertools-11.1.0-py3-none-any.whl", hash = "sha256:4b65538ae22f6fed0ce4874efd317463a7489796a0939fa66824dd542125a192", size = 72226, upload-time = "2026-05-22T14:14:28.824Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nh3"
|
||||||
|
version = "0.3.7"
|
||||||
|
source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" }
|
||||||
|
sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/18/2f/022b27146d52d24b1b353b003359134788ecbcd6fcdf6283adbd57c0fbc8/nh3-0.3.7.tar.gz", hash = "sha256:71860d01c16f4d8c72e334e0674beb2b0899dbd0bf760de18932ef4390303848", size = 25662, upload-time = "2026-08-23T14:26:30.728Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/ce/88/b594f0e86856b37e182fb663283da419eea6424972506e640e890885467f/nh3-0.3.7-cp314-cp314t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:91a4dab4e94d9fc54b9f67b1adfb23e81fab7ab43f33c3b8c97be9aa38f789ba", size = 1471147, upload-time = "2026-08-23T14:25:55.259Z" },
|
||||||
|
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/1e/60/847a21339f095c4d4c655af31fa2d18b174585bcc210709facacc7ce205c/nh3-0.3.7-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eae64328e46a25785535afcb6885b6f182ecaf5ee8c88f8c075422db8aacc65b", size = 820463, upload-time = "2026-08-23T14:25:56.803Z" },
|
||||||
|
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/7b/7f/1a103e00aaf5e59f2dee4c2709aac609bb2d4bb74fddaf0dcfade11ed87b/nh3-0.3.7-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4968fe8d2db97c6f047659bf46a449fd8ec377f44ebf3e0a1b96c0d3a333ae32", size = 861456, upload-time = "2026-08-23T14:25:58.087Z" },
|
||||||
|
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/d8/4a/e9c436089a0c80b928011ead0efd156aa7639a19b6064ef58dcedcab8369/nh3-0.3.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:be53a4825585f701955cb9baf49f478f56eb81e20294329fe4bc689dd5dd81fa", size = 1023930, upload-time = "2026-08-23T14:25:59.465Z" },
|
||||||
|
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/04/5c/aa1468e3e281e78d2b3b7d762ccba59f681af355e971dbd255d5903f7b86/nh3-0.3.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:94fd6e59553fbb9ffd8ba71bbd5a54e3126ba01799a097ae30d5341d750bc6ac", size = 1102614, upload-time = "2026-08-23T14:26:00.869Z" },
|
||||||
|
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/6a/9f/57d186d9d3dd38905dc12dddb3484406cdf6aa0b1ce33639a2d277d4ee1c/nh3-0.3.7-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:18f4278ecd157d43cb35acd5aae9f35cfa79f546b4922bd86536adc0f6312102", size = 1059915, upload-time = "2026-08-23T14:26:02.388Z" },
|
||||||
|
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/6b/53/097a5ad0b34b15d67a472ef849165a54209fa5fbd3e639801c6fe439ba28/nh3-0.3.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:808def0c8c07843e6e50dc84f532457bfa2cfd17417b219a5d9e7c773709331a", size = 1047402, upload-time = "2026-08-23T14:26:03.897Z" },
|
||||||
|
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/9a/a7/c57a2c70534418310889a65ccfac3525e62f0bc0a8613225903403755ce7/nh3-0.3.7-cp314-cp314t-win32.whl", hash = "sha256:874b7d67a067bd29a59223f6270fc30da4edd8e6d87fd219fc93bcbaa662c946", size = 619895, upload-time = "2026-08-23T14:26:05.105Z" },
|
||||||
|
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/e6/b7/efda1d0a611d940bdfde6893bde1ea6b7b7d48c31273aea48e35b822fd58/nh3-0.3.7-cp314-cp314t-win_amd64.whl", hash = "sha256:614dac4a4c36ad084e78447d16fe898dedd762e354a7ab9cda2984e82f67883d", size = 633456, upload-time = "2026-08-23T14:26:06.661Z" },
|
||||||
|
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/1d/18/3ab564595cb88196f50d26e163ed0fd2acc731ab26ac615df91981885887/nh3-0.3.7-cp314-cp314t-win_arm64.whl", hash = "sha256:157ec1eb7a62f3d9a7badb8d82d89aa810e3e24e097eedfa481a25d0c8a99877", size = 611003, upload-time = "2026-08-23T14:26:07.813Z" },
|
||||||
|
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/94/0d/c257754bf57f829f307aa226bbe136d3a1356b5a0d08324c7b6bd2a8aacd/nh3-0.3.7-cp38-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:6c3aa50eb26e9228238271db9f983cbc3b006dfbfeca2d4dc34c33ddc6ac5ea5", size = 1493959, upload-time = "2026-08-23T14:26:09.025Z" },
|
||||||
|
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/07/42/a687e7091928806e514f89fa2666f25ec9bfe0a902fc4402b25e51ce408b/nh3-0.3.7-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f266d3f1b3647449923a8e406524632220dd5d8b647078dfe45b885d33d10479", size = 859615, upload-time = "2026-08-23T14:26:10.606Z" },
|
||||||
|
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/85/05/b0e6bef633549a23347d5462aa288fcc42381e7918482062ca3cb456242a/nh3-0.3.7-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e8fd1ab205258b29254f72db377d99e2c96aa7653ef3b015ccab0420b094b506", size = 839872, upload-time = "2026-08-23T14:26:12.037Z" },
|
||||||
|
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/17/40/2a0921d45b20828708bcb56887e47dcf8cae13818de5bf9a01308d348712/nh3-0.3.7-cp38-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:19f288c938ec6eef1f5d2c6cab47838e71fef8097e1c1233802be5a6230ba086", size = 1091325, upload-time = "2026-08-23T14:26:13.34Z" },
|
||||||
|
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/e4/d1/9d70e0e418a48280ec0ddc6c1b08b4b1136ebcc31a1625e57ff5c665fa51/nh3-0.3.7-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de2b2aab32ea303405debefdcfc58043d3e635fa3f67b9eb140d2b0e0c0d2563", size = 1042482, upload-time = "2026-08-23T14:26:14.667Z" },
|
||||||
|
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/93/a7/02dd159d4e71f98607d8d4249cddb7561e77be1a8e4dec77d76e1b68fc99/nh3-0.3.7-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b7279d43323a25225df23576af6594a16693f61431170848b8b2ac21ad4f174", size = 946868, upload-time = "2026-08-23T14:26:16.094Z" },
|
||||||
|
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/a6/ed/c5510c615dce55b6fcc364aa1838142f938beed64f5e4927490dfcaf4405/nh3-0.3.7-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70f5ac8626e899a4bab0ef74ca2f5bd602f49c7b739e6e5026b4afc6d63dac42", size = 832161, upload-time = "2026-08-23T14:26:17.272Z" },
|
||||||
|
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/7b/e3/3212c1a5b5745245d7f18885207bbddb34c56075f34dd682bd539aad55cc/nh3-0.3.7-cp38-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:5ffdfcb9a686ffb12765376bcfb6b5b55728516d3c0ee317d29982381ded3df8", size = 849791, upload-time = "2026-08-23T14:26:18.498Z" },
|
||||||
|
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/20/64/9e36594efad6c290de4240d02cb2bd80c339a4ab1c4de66e599ffa6d9d81/nh3-0.3.7-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bc42bb1193c1e28a1e74c2cabaca178e118a7103e8832699fef8a2b3e2496493", size = 875473, upload-time = "2026-08-23T14:26:19.908Z" },
|
||||||
|
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/00/0c/1a8985fd43fea5530c0ac890b6f0b423770ee72f111b70b7a77f2dec243a/nh3-0.3.7-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:d56e76bd3cadb09b6b0cef364850811663734b348a25f5f587a2819c495367bd", size = 1036463, upload-time = "2026-08-23T14:26:21.536Z" },
|
||||||
|
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/b2/5d/891e533b716cf00df76ad0ba6485dcfd14d59a6430a3cc99057c4c04004e/nh3-0.3.7-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:fd4a70efb45d5372174f718878eb7a35c12677626a63b2f103b23b833457dcac", size = 1116029, upload-time = "2026-08-23T14:26:22.907Z" },
|
||||||
|
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/42/e5/ae8c0782fce74fb6fcf7234bb3d4017f37ce181b4f9d29369eab21c50a04/nh3-0.3.7-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:15f5fbf090f5c88d61c820e1fc1fceecb6520cca9fe85649c06b57ef9dc9ff62", size = 1076589, upload-time = "2026-08-23T14:26:24.302Z" },
|
||||||
|
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/26/a4/c3423351e8d864ad756e85e15f0c01433361f14d34e4ed156482c0518f2a/nh3-0.3.7-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6698a822132beedab80f131c08d8d0ac5a178ddeb488d02ca4b67716ecfac7af", size = 1058871, upload-time = "2026-08-23T14:26:25.674Z" },
|
||||||
|
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/4b/6a/478f153f1d7c0baaa3d1e8bb5fdcee3a6235f90fe44ea969a9d4e2b8c47a/nh3-0.3.7-cp38-abi3-win32.whl", hash = "sha256:6e4280115d44c3b278eef712a86748c1a723105cd79feec46952383117ab4e59", size = 630729, upload-time = "2026-08-23T14:26:26.932Z" },
|
||||||
|
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/b4/b9/34433ccb1f0fe6968dabbb7d4bf5721c6221878ef07832748c06655a6a80/nh3-0.3.7-cp38-abi3-win_amd64.whl", hash = "sha256:618e3059caf41ccdf5dcccb3fa9df4cf6e4efe23d1382a8bbfca272a8a4f8bfc", size = 644462, upload-time = "2026-08-23T14:26:28.294Z" },
|
||||||
|
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/f9/70/e140dffff6e808dc6343598df76e7e2407fd0f581de3524c75fba2e0cf24/nh3-0.3.7-cp38-abi3-win_arm64.whl", hash = "sha256:f04b7d333b27f13ca439da3cf1c75c2fba34f104969f6ce4ac8e7079699c2f4a", size = 621867, upload-time = "2026-08-23T14:26:29.547Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nuitka"
|
name = "nuitka"
|
||||||
version = "4.1.3"
|
version = "4.1.3"
|
||||||
@@ -1569,6 +1710,20 @@ wheels = [
|
|||||||
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" },
|
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "readme-renderer"
|
||||||
|
version = "45.0"
|
||||||
|
source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "docutils" },
|
||||||
|
{ name = "nh3" },
|
||||||
|
{ name = "pygments" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/02/51/d3a6ea424652c60f05600d8c2e01a55c913755e7cdad64afabbd1aa16f44/readme_renderer-45.0.tar.gz", hash = "sha256:030a8fac74904f8fba11ad1bb6964e3f76e896dc7e5e71f16af190c9056696d1", size = 36172, upload-time = "2026-06-09T21:05:17.37Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/97/1b/295bf2fa3e740131778065e5ffa2c481f0e7210182d408e9a2c244ff5b0c/readme_renderer-45.0-py3-none-any.whl", hash = "sha256:3385ed220117104a2bceb4a9dac8c5fdf6d1f96890d7ea2a9c7174fd5c84091f", size = 14134, upload-time = "2026-06-09T21:05:15.85Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "redis"
|
name = "redis"
|
||||||
version = "8.1.0"
|
version = "8.1.0"
|
||||||
@@ -1593,6 +1748,27 @@ wheels = [
|
|||||||
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" },
|
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "requests-toolbelt"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "requests" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/f3/61/d7545dafb7ac2230c70d38d31cbfe4cc64f7144dc41f6e4e4b78ecd9f5bb/requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", size = 206888, upload-time = "2023-05-01T04:11:33.229Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06", size = 54481, upload-time = "2023-05-01T04:11:28.427Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rfc3986"
|
||||||
|
version = "2.0.0"
|
||||||
|
source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" }
|
||||||
|
sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/85/40/1520d68bfa07ab5a6f065a186815fb6610c86fe957bc065754e47f7b0840/rfc3986-2.0.0.tar.gz", hash = "sha256:97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c", size = 49026, upload-time = "2022-01-10T00:52:30.832Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/ff/9a/9afaade874b2fa6c752c36f1548f718b5b83af81ed9b76628329dab81c1b/rfc3986-2.0.0-py2.py3-none-any.whl", hash = "sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd", size = 31326, upload-time = "2022-01-10T00:52:29.594Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rich"
|
name = "rich"
|
||||||
version = "15.0.0"
|
version = "15.0.0"
|
||||||
@@ -1631,6 +1807,19 @@ wheels = [
|
|||||||
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/0e/fe/da8b9e1347696bb22120b77280ec5ce25d500ca5cb39d5ad6e5c18de19c1/ruff-0.16.4-py3-none-win_arm64.whl", hash = "sha256:a3a61621c9b6f6a89573e938a080e648f1695baa3f58570a3a707bc51ff65a21", size = 10451579, upload-time = "2026-08-20T17:43:57.135Z" },
|
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/0e/fe/da8b9e1347696bb22120b77280ec5ce25d500ca5cb39d5ad6e5c18de19c1/ruff-0.16.4-py3-none-win_arm64.whl", hash = "sha256:a3a61621c9b6f6a89573e938a080e648f1695baa3f58570a3a707bc51ff65a21", size = 10451579, upload-time = "2026-08-20T17:43:57.135Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "secretstorage"
|
||||||
|
version = "3.5.0"
|
||||||
|
source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "cryptography" },
|
||||||
|
{ name = "jeepney" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/1c/03/e834bcd866f2f8a49a85eaff47340affa3bfa391ee9912a952a1faa68c7b/secretstorage-3.5.0.tar.gz", hash = "sha256:f04b8e4689cbce351744d5537bf6b1329c6fc68f91fa666f60a380edddcd11be", size = 19884, upload-time = "2025-11-23T19:02:53.191Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/b7/46/f5af3402b579fd5e11573ce652019a67074317e18c1935cc0b4ba9b35552/secretstorage-3.5.0-py3-none-any.whl", hash = "sha256:0ce65888c0725fcb2c5bc0fdb8e5438eece02c523557ea40ce0703c266248137", size = 15554, upload-time = "2025-11-23T19:02:51.545Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "setuptools"
|
name = "setuptools"
|
||||||
version = "84.0.0"
|
version = "84.0.0"
|
||||||
@@ -1813,6 +2002,26 @@ wheels = [
|
|||||||
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/19/97/56608b2249fe206a67cd573bc93cd9896e1efb9e98bce9c163bcdc704b88/truststore-0.10.4-py3-none-any.whl", hash = "sha256:adaeaecf1cbb5f4de3b1959b42d41f6fab57b2b1666adb59e89cb0b53361d981", size = 18660, upload-time = "2025-08-12T18:49:01.46Z" },
|
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/19/97/56608b2249fe206a67cd573bc93cd9896e1efb9e98bce9c163bcdc704b88/truststore-0.10.4-py3-none-any.whl", hash = "sha256:adaeaecf1cbb5f4de3b1959b42d41f6fab57b2b1666adb59e89cb0b53361d981", size = 18660, upload-time = "2025-08-12T18:49:01.46Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "twine"
|
||||||
|
version = "7.0.0"
|
||||||
|
source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "id" },
|
||||||
|
{ name = "keyring", marker = "platform_machine != 'ppc64le' and platform_machine != 's390x'" },
|
||||||
|
{ name = "packaging" },
|
||||||
|
{ name = "readme-renderer" },
|
||||||
|
{ name = "requests" },
|
||||||
|
{ name = "requests-toolbelt" },
|
||||||
|
{ name = "rfc3986" },
|
||||||
|
{ name = "rich" },
|
||||||
|
{ name = "urllib3" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/92/3c/58f808a359700f39a967dffede33efeac809262c03303fa3eec6afff8f49/twine-7.0.0.tar.gz", hash = "sha256:85cdb29c518efef867360ae4acd4b0dfd61c8654a22fca08e6f8539f05022177", size = 215032, upload-time = "2026-07-27T15:59:00.825Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/96/08/ddcdc06225eaad6de0e48e1002b06d919dbde20582d0662c7af51308e5d6/twine-7.0.0-py3-none-any.whl", hash = "sha256:b854164df26db268af05f49aa5c0344b10e27a494343ff05b1e0bad3b135f5a7", size = 43204, upload-time = "2026-07-27T15:58:59.26Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typer"
|
name = "typer"
|
||||||
version = "0.27.1"
|
version = "0.27.1"
|
||||||
|
|||||||
Reference in New Issue
Block a user