modify: diskcache 缓存替换

This commit is contained in:
2026-04-13 15:55:12 +08:00
parent 6685bdb9a8
commit ee0a5cf0f6
26 changed files with 824 additions and 288 deletions
BIN
View File
Binary file not shown.
+9 -4
View File
@@ -1,7 +1,7 @@
[default]
app.name = "chongming"
app.version = "chongming"
app.description = "基于 FastAPI 和 Tortoise-ORM 的 chongming 打包脚手架"
app.description = "基于 FastAPI 的 chongming 打包脚手架"
app.debug = false
env = "development"
@@ -20,12 +20,17 @@ pool_pre_ping = true
connect_args.timeout = 15
connect_args.check_same_thread = false
[scheduler]
job_store_path = "scheduler_jobs.db"
[cache]
cache_store_path = "./"
[security]
secret_key = "your-secret-key-change-in-production"
algorithm = "HS256"
access_token_expire_minutes = 30
refresh_token_expire_days = 7
jwt_cache_db_path = "jwt_cache"
max_sessions_per_user = 5
[development]
@@ -39,7 +44,7 @@ reload = true
[development.logging]
level = "DEBUG"
file = "logs/dev.log"
file = "logs"
format = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
[development.cors]
@@ -60,7 +65,7 @@ workers = 4
[production.logging]
level = "INFO"
file = "logs/prod.log"
file = "logs"
format = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
max_size = 10485760 # 10MB
backup_count = 5 # 最多保存 10 个日志文件
BIN
View File
Binary file not shown.
+8 -1
View File
@@ -20,12 +20,17 @@ pool_pre_ping = true
connect_args.timeout = 15
connect_args.check_same_thread = false
[scheduler]
job_store_path = "scheduler_jobs.db"
[cache]
cache_store_path = "./"
[security]
secret_key = "your-secret-key-change-in-production"
algorithm = "HS256"
access_token_expire_minutes = 30
refresh_token_expire_days = 7
jwt_cache_db_path = "jwt_cache"
max_sessions_per_user = 5
[development]
@@ -62,6 +67,8 @@ workers = 4
level = "INFO"
file = "logs/prod.log"
format = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
max_size = 10485760 # 10MB
backup_count = 5 # 最多保存 10 个日志文件
[production.cors]
allow_origins = ["https://yourdomain.com", "http://localhost:8000", "http://127.0.0.1:8000"]
+2 -3
View File
@@ -1,7 +1,7 @@
# main.py - 修复 reload 问题
#!/usr/bin/env python3
"""
FastAPI + Tortoise-ORM + chongming 示例
FastAPI chongming 示例
主入口文件,适配 chongming 打包系统
"""
import sys
@@ -19,9 +19,8 @@ def app_run(app_config: dict, default_config: dict):
default_config: 默认配置
"""
import uvicorn
from app.core.logger import setup_logging, get_logger # type: ignore
from app.core.logger import get_logger # type: ignore
setup_logging(app_config["logging"])
logger = get_logger("app")
# 获取服务器配置
+2 -1
View File
@@ -10,13 +10,14 @@ readme = "README.md"
requires-python = ">=3.8"
dependencies = [
"aiosqlite",
"apscheduler",
"bcrypt==4.0.1",
"diskcache>=5.6.3",
"fastapi[uvicorn]",
"module-bank",
"passlib[bcrypt]",
"pyjwt",
"sqlite-vfs",
"sqlitedict",
"sqlmodel[aiosqlite]",
"tomli",
"typing-extensions",
+1 -1
View File
@@ -4,6 +4,6 @@
"pythonVersion": "3.11",
"extraPaths": [
"./",
"./.venv/lib/python3.12/site-packages"
"./.venv/lib/python3.11/site-packages"
]
}
+12 -15
View File
@@ -1,16 +1,13 @@
aerich
fastapi[uvicorn]
module-bank==0.1.1
passlib
pydantic-settings
pydantic[email]
sqlite-vfs
toml
tortoise-orm
uvicorn[standard]
sqlitedict
pyjwt
aiosqlite
apscheduler
bcrypt==4.0.1
pyinstaller
aerich
requests
fastapi[uvicorn]
module-bank
passlib[bcrypt]
pyjwt
sqlite-vfs
sqlitedict
sqlmodel[aiosqlite]
tomli
typing-extensions
uvicorn
BIN
View File
Binary file not shown.
-1
View File
@@ -8,7 +8,6 @@ def serve():
env = config["default"]["env"]
server_config = config[env]["server"]
log_config = config[env]["logging"]
setup_logging(log_config)
logget = get_logger("app")
logget.info(f"启动服务: {server_config}")
logget.info(f"启动日志: {log_config}")
+42 -7
View File
@@ -1,15 +1,26 @@
from contextlib import asynccontextmanager
from fastapi import FastAPI
from fastapi import FastAPI, Request, Depends
from fastapi.responses import JSONResponse
from fastapi.middleware.cors import CORSMiddleware
from sqlmodel import SQLModel, text
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
from sqlalchemy.exc import OperationalError
from pydantic import BaseModel
from .core.config import get_config
from .core.jwt_cache import get_jwt_cache
from .core.cache import get_cache
from .core.logger import get_logger
from .core.scheduler import get_task_service, get_task_service_instance, TaskService
from .api import api_router
from .task.execute_background import execute_background_task
from .task import init_tasks_callback
class TaskRequest(BaseModel):
task_name: str
interval: int
config = get_config()
logger = get_logger("app")
@@ -32,6 +43,12 @@ async def lifespan(app: FastAPI):
if database_config is None:
raise ValueError("配置不存在")
task_service = get_task_service_instance()
await task_service.start(init_tasks_callback)
app.state.task_service = task_service
# 启动时:创建引擎和会话工厂,初始化数据库表
engine = create_async_engine(database["url"], **database_config)
async_session_maker = async_sessionmaker(
@@ -61,8 +78,9 @@ async def lifespan(app: FastAPI):
yield # 应用运行期间
# 关闭时:释放引擎
await task_service.shutdown(wait=True)
await engine.dispose()
get_jwt_cache().close()
get_cache().close()
cors = config[env].get("cors", {})
@@ -87,19 +105,36 @@ app.include_router(api_router, prefix="/api/v1")
async def root():
"""根路径,返回应用信息"""
return {
"message": f"欢迎使用 {config["default"]["app"]["name"]}",
"message": f"欢迎使用 {config['default']['app']['name']}",
"version": config["default"]["app"]["version"],
"docs": "/docs" if config[env]["debug"] else None,
"health": "/api/v1/health",
}
@app.post("/schedule")
async def add_task(
request: TaskRequest, task_service: TaskService = Depends(get_task_service)
):
"""添加定时任务接口"""
job = await task_service.add_interval_job(
execute_background_task,
seconds=request.interval,
args=[request.task_name],
job_id=request.task_name,
)
return {"status": "success", "job_id": job.id}
# 全局异常处理器
@app.exception_handler(Exception)
async def global_exception_handler(request, exc):
async def global_exception_handler(_: Request, exc: Exception):
"""全局异常处理器"""
logger.error(f"全局异常: {exc}")
return {
return JSONResponse(
status_code=500,
content={
"error": "内部服务器错误",
"detail": str(exc) if config[env]["debug"] else "请查看服务器日志",
}
},
)
+2 -2
View File
@@ -5,11 +5,11 @@ from ..service.auth import get_auth_service
from ..core.jwt_cache import TokenData
security = HTTPBearer()
access_scheme = HTTPBearer(scheme_name="AccessToken")
async def get_current_user(
credentials: HTTPAuthorizationCredentials = Depends(security),
credentials: HTTPAuthorizationCredentials = Depends(access_scheme),
) -> TokenData:
"""
获取当前用户依赖项
+8 -22
View File
@@ -3,7 +3,7 @@ from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from sqlalchemy.ext.asyncio import AsyncSession
from ...core.database import get_session
from ...core.jwt_cache import get_jwt_cache, TokenData
from ...core.jwt_cache import TokenData
from ...core.security import verify_password
from ...service.user import UserService
from ...service.auth import get_auth_service
@@ -13,7 +13,8 @@ from ..deps import get_current_user
router = APIRouter(tags=["authentication"])
security = HTTPBearer()
access_scheme = HTTPBearer(scheme_name="AccessToken")
refresh_scheme = HTTPBearer(scheme_name="RefreshToken")
@router.post("/register", response_model=UserRead)
@@ -50,29 +51,14 @@ async def login(
raise HTTPException(status_code=400, detail="邮箱或密码错误")
if not user.is_active:
raise HTTPException(status_code=400, detail="用户被禁用")
cache = get_jwt_cache()
user_data_dict = {
"sub": str(user.id),
"email": user.email,
"username": user.username,
}
token = cache.create_token(
user_id=str(user.id),
user_data=user_data_dict,
token_type="access",
device_id=None,
user_agent=request.headers.get("user-agent") if request else None,
ip_address=request.client.host if request else None, # type: ignore
)
return {"access_token": token, "token_type": "bearer"}
auth_service = get_auth_service()
return await auth_service.create_tokens(user, request, device_id=None)
@router.post("/refresh", response_model=dict, summary="刷新访问令牌")
async def refresh_token(
request: Request,
credentials: HTTPAuthorizationCredentials = Depends(security),
credentials: HTTPAuthorizationCredentials = Depends(refresh_scheme),
session: AsyncSession = Depends(get_session),
):
auth_service = get_auth_service()
@@ -92,7 +78,7 @@ async def refresh_token(
@router.post("/logout")
async def logout(
credentials: HTTPAuthorizationCredentials = Depends(security),
credentials: HTTPAuthorizationCredentials = Depends(access_scheme),
):
auth_service = get_auth_service()
token = credentials.credentials
@@ -100,7 +86,7 @@ async def logout(
return {"message": "登出成功"}
@router.post("/logout-all", summary="等出所有设备")
@router.post("/logout-all", summary="登出所有设备")
async def logout_all(current_user: TokenData = Depends(get_current_user)):
auth_service = get_auth_service()
await auth_service.logout_all(current_user.user_id)
+6 -12
View File
@@ -10,11 +10,11 @@ from ...model.user import User
from ..deps import get_current_user
router = APIRouter()
router = APIRouter(dependencies=[Depends(get_current_user)])
# -------------------- 路由 --------------------
@router.post("/", response_model=TodoRead, dependencies=[Depends(get_current_user)])
@router.post("/", response_model=TodoRead)
async def create_todo(
todo: TodoCreate,
session: AsyncSession = Depends(get_session),
@@ -22,18 +22,14 @@ async def create_todo(
return await TodoService.create_todo(todo, session)
@router.get(
"/", response_model=List[TodoRead], dependencies=[Depends(get_current_user)]
)
@router.get("/", response_model=List[TodoRead])
async def read_todos(
offset: int = 0, limit: int = 100, session: AsyncSession = Depends(get_session)
):
return await TodoService.get_todos(offset, limit, session)
@router.get(
"/{todo_id}", response_model=TodoRead, dependencies=[Depends(get_current_user)]
)
@router.get("/{todo_id}", response_model=TodoRead)
async def read_todo(todo_id: int, session: AsyncSession = Depends(get_session)):
todo = await TodoService.get_todo_by_id(todo_id, session)
if not todo:
@@ -41,9 +37,7 @@ async def read_todo(todo_id: int, session: AsyncSession = Depends(get_session)):
return todo
@router.patch(
"/{todo_id}", response_model=TodoRead, dependencies=[Depends(get_current_user)]
)
@router.patch("/{todo_id}", response_model=TodoRead)
async def update_todo(
todo_id: int, todo_update: TodoUpdate, session: AsyncSession = Depends(get_session)
):
@@ -51,7 +45,7 @@ async def update_todo(
return todo
@router.delete("/{todo_id}", dependencies=[Depends(get_current_user)])
@router.delete("/{todo_id}")
async def delete_todo(todo_id: int, session: AsyncSession = Depends(get_session)):
is_deleted = await TodoService.delete_todo(todo_id, session)
if not is_deleted:
+248
View File
@@ -0,0 +1,248 @@
import time
from typing import Any, Optional
import diskcache
from ..core.config import get_config
class Cache:
def __init__(self):
config = get_config()
cache_dir = config["cache"]["cache_store_path"]
self._cache = diskcache.Cache(cache_dir)
def set(
self,
key: str,
value: Any,
ex: Optional[int] = None,
px: Optional[int] = None,
raw: bool = False,
) -> bool:
"""
设置缓存
Args:
key: 键
value: 值
ex: 过期时间(秒)
px: 过期时间(毫秒)
Returns:
bool: 是否设置成功
"""
try:
expire_at = None
expire_seconds = None
if ex is not None:
expire_seconds = ex
expire_at = time.time() + ex
elif px is not None:
expire_seconds = px / 1000.0
expire_at = time.time() + expire_seconds
stored_value = (value, expire_at)
self._cache.set(key, stored_value, expire=expire_seconds)
return True
except Exception as e:
print(f"[缓存] set 错误: {e}")
return False
def get(self, key: str, default: Any = None) -> Any:
"""
获取缓存
Args:
key: 键
default: 默认值
Returns:
缓存的值,如果key不存在或已过期返回default
"""
try:
stored = self._cache.get(key)
if stored is None:
return default
value, expire_at = stored # type: ignore
if expire_at is not None and time.time() > expire_at: # type: ignore
self._cache.delete(key)
return default
return value
except Exception as e:
print(f"[缓存] get 错误: {e}")
return default
def delete(self, *keys: str) -> int:
"""
删除一个或多个key
Args:
keys: 要删除的key
Returns:
成功删除的数量
"""
deleted = 0
for key in keys:
if self._cache.delete(key):
deleted += 1
return deleted
def exists(self, key: str) -> bool:
"""
检查key是否存在且未过期
Args:
key: 键
Returns:
bool: 是否存在
"""
return self.get(key, None) is not None
def expire(self, key: str, _time: int) -> bool:
"""
设置过期时间
Args:
key: 键
time: 过期时间(秒)
Returns:
bool: 是否设置成功
"""
try:
stored = self._cache.get(key)
if stored is None:
return False
value, _ = stored # type: ignore
expire_at = time.time() + _time
new_stored = (value, expire_at)
self._cache.set(key, new_stored, expire=_time)
return True
except Exception as e:
print(f"[缓存] expire 错误: {e}")
return False
def ttl(self, key: str) -> int:
"""
获取key的剩余存活时间
Args:
key: 键
Returns:
int: 剩余秒数,-1表示永不过期,-2表示key不存在
"""
try:
stored = self._cache.get(key)
if stored is None:
return -2
_, expire_at = stored # type: ignore
if expire_at is None:
return -1
remaining = expire_at - time.time() # type: ignore
return max(0, int(remaining))
except Exception as e:
print(f"[缓存] ttl 错误: {e}")
return -2
def keys(self, pattern: Optional[str] = None) -> list:
"""
获取所有key,支持简单的通配符匹配(*)
Args:
pattern: 匹配模式,如 "user:*"
Returns:
list: key列表
"""
all_keys = list(self._cache.iterkeys())
if pattern and "*" in pattern:
prefix = pattern.replace("*", "")
all_keys = [k for k in all_keys if k.startswith(prefix)] # type: ignore
return all_keys
def clear(self) -> bool:
"""清空所有缓存。"""
try:
self._cache.clear()
return True
except Exception as e:
print(f"[缓存] clear 错误: {e}")
return False
def size(self) -> int:
"""获取当前缓存数量(不含过期但尚未清理的项)。"""
# diskcache 的 __len__ 会返回当前有效条目数
return len(list(self._cache.iterkeys()))
def get_all(self) -> dict:
"""获取所有未过期的缓存数据。"""
result = {}
for key in self._cache.iterkeys():
value = self.get(str(key)) # get 内部会检查并清理过期
if value is not None:
result[key] = value
return result
def close(self):
"""关闭缓存。"""
self._cache.close()
_cache: Optional[Cache] = None
def get_cache() -> Cache:
global _cache
if _cache is None:
_cache = Cache()
return _cache
# 装饰器:缓存函数结果
def cached(ttl: Optional[int] = None):
"""
函数结果缓存装饰器
Args:
ttl: 缓存时间(秒),None表示永不过期
Example:
@cached(ttl=60)
def get_user(user_id: int):
return {"id": user_id, "name": "test"}
"""
def decorator(func):
def wrapper(*args, **kwargs):
# 生成缓存key
key = f"{func.__name__}:{str(args)}:{str(kwargs)}"
cache = get_cache()
# 尝试从缓存获取
result = cache.get(key)
if result is not None:
return result
# 执行函数
result = func(*args, **kwargs)
# 存入缓存
cache.set(key, result, ex=ttl)
return result
return wrapper
return decorator
+16 -7
View File
@@ -1,6 +1,6 @@
import sys
from pathlib import Path
from typing import TypedDict, Literal, Optional, cast
from typing import TypedDict, Literal, Optional, cast, List
if sys.version_info >= (3, 11):
@@ -53,7 +53,6 @@ class Security(TypedDict):
secret_key: str
algorithm: Literal["HS256"]
access_token_expire_minutes: int
jwt_cache_db_path: str
refresh_token_expire_days: int
max_sessions_per_user: int
@@ -74,20 +73,20 @@ class Logging(TypedDict):
class Cors(TypedDict, total=False):
allow_origins: list[str]
allow_origins: List[str]
allow_credentials: bool
allow_methods: list[str]
allow_headers: list[str]
allow_methods: List[str]
allow_headers: List[str]
class ModuleSystem(TypedDict, total=False):
type: str
path: list[str]
path: List[str]
class FileSystem(TypedDict, total=False):
type: str
path: list[str]
path: List[str]
class Env(TypedDict, total=False):
@@ -99,9 +98,19 @@ class Env(TypedDict, total=False):
file_system: FileSystem
class Scheduler(TypedDict):
job_store_path: str
class Cache(TypedDict):
cache_store_path: str
class Config(TypedDict):
default: Default
database: Database
scheduler: Scheduler
cache: Cache
security: Security
development: Env
production: Env
+39 -122
View File
@@ -1,17 +1,15 @@
import jwt
import json
from datetime import datetime, timedelta
from typing import Optional, Dict, Any, List, TypedDict
from sqlitedict import SqliteDict
from typing import Optional, Dict, Any, List
from pydantic import BaseModel
from dataclasses import dataclass, asdict
# from pydantic import BaseModel
import hashlib
import threading
from pathlib import Path
import jwt
from .config import get_config
from .cache import get_cache
@dataclass
@@ -43,43 +41,9 @@ class JWTCache:
def __init__(self, db_path: Optional[str] = None):
self.config = get_config()
if db_path is None:
db_path = self.config["security"]["jwt_cache_db_path"]
db_file_path = Path(db_path)
db_file_path.mkdir(parents=True, exist_ok=True)
self.lock = threading.RLock()
# 主 Token 缓存
self.token_db = SqliteDict(
rf"{db_path}/tokens.cache",
tablename="tokens",
autocommit=True,
encode=self._json_encode,
decode=self._json_decode,
journal_mode="WAL",
)
# 黑名单缓存
self.blacklist_db = SqliteDict(
rf"{db_path}/blacklist.cache",
tablename="blacklist",
autocommit=True,
encode=json.dumps,
decode=json.loads,
journal_mode="WAL",
)
# 用户会话索引
self.user_sessions_db = SqliteDict(
rf"{db_path}/user_sessions.cache",
tablename="user_sessions",
autocommit=True,
encode=json.dumps,
decode=json.loads,
journal_mode="WAL",
)
self.cache = get_cache()
self.secret_key = self.config["security"]["secret_key"]
self.algorithm = self.config["security"]["algorithm"]
@@ -168,7 +132,7 @@ class JWTCache:
)
# 存储到缓存
self.token_db[token_hash] = asdict(cache_data)
self.cache.set(f"token:{token_hash}", asdict(cache_data))
# 更新用户会话索引
self._update_user_session_index(user_id, token_hash, token_type)
@@ -189,8 +153,8 @@ class JWTCache:
):
"""更新用户会话索引"""
with self.lock:
key = f"{user_id}:{token_type}"
user_tokens = self.user_sessions_db.get(key, [])
key = f"user:{user_id}:{token_type}"
user_tokens = self.cache.get(key, [])
# 添加新 token
user_tokens.append(token_hash)
@@ -200,36 +164,36 @@ class JWTCache:
if len(user_tokens) > max_tokens:
user_tokens = user_tokens[-max_tokens:]
self.user_sessions_db[key] = user_tokens
self.cache.set(key, user_tokens)
def _enforce_session_limit(self, user_id: str):
"""强制用户会话限制"""
max_sessions = self.config["security"]["max_sessions_per_user"]
access_key = f"{user_id}:access"
refresh_key = f"{user_id}:refresh"
access_key = f"user:{user_id}:access"
refresh_key = f"user:{user_id}:refresh"
# 检查 access token 数量
access_tokens = self.user_sessions_db.get(access_key, [])
access_tokens = self.cache.get(access_key, [])
if len(access_tokens) > max_sessions:
# 删除最早的 token
tokens_to_remove = access_tokens[:-max_sessions]
for token_hash in tokens_to_remove:
if token_hash in self.token_db:
del self.token_db[token_hash]
if self.cache.exists(f"token:{token_hash}"):
self.cache.delete(f"token:{token_hash}")
# 更新索引
self.user_sessions_db[access_key] = access_tokens[-max_sessions:]
self.cache.set(access_key, access_tokens[-max_sessions:])
# 检查 refresh token 数量
refresh_tokens = self.user_sessions_db.get(refresh_key, [])
refresh_tokens = self.cache.get(refresh_key, [])
if len(refresh_tokens) > max_sessions:
tokens_to_remove = refresh_tokens[:-max_sessions]
for token_hash in tokens_to_remove:
if token_hash in self.token_db:
del self.token_db[token_hash]
if self.cache.exists(f"token:{token_hash}"):
self.cache.delete(f"token:{token_hash}")
self.user_sessions_db[refresh_key] = refresh_tokens[-max_sessions:]
self.cache.set(refresh_key, refresh_tokens[-max_sessions:])
def validate_token(
self, token: str, token_type: str = "access"
@@ -259,14 +223,11 @@ class JWTCache:
if not user_id or not jti:
return None
# 检查黑名单
token_hash = self._hash_token(token)
if self._is_blacklisted(token_hash):
return None
# 检查缓存
with self.lock:
cache_data = self.token_db.get(token_hash)
cache_data = self.cache.get(f"token:{token_hash}")
if not cache_data:
return None
@@ -279,12 +240,12 @@ class JWTCache:
if datetime.utcnow() > expires_at:
# 标记为不活跃
cache_data["is_active"] = False
self.token_db[token_hash] = cache_data
self.cache.set(f"token:{token_hash}", cache_data)
return None
# 更新最后使用时间
cache_data["last_used"] = datetime.utcnow().isoformat()
self.token_db[token_hash] = cache_data
self.cache.set(f"token:{token_hash}", cache_data)
return {
"user_id": user_id,
@@ -300,68 +261,38 @@ class JWTCache:
except Exception:
return None
def add_to_blacklist(self, token: str, reason: str = "logout"):
"""添加 Token 到黑名单"""
token_hash = self._hash_token(token)
expire_time = datetime.utcnow() + timedelta(days=7) # 黑名单保留7天
self.blacklist_db[token_hash] = {
"reason": reason,
"blacklisted_at": datetime.utcnow().isoformat(),
"expires_at": expire_time.isoformat(),
}
def _is_blacklisted(self, token_hash: str) -> bool:
"""检查 Token 是否在黑名单中"""
with self.lock:
if token_hash not in self.blacklist_db:
return False
blacklist_data = self.blacklist_db[token_hash]
expires_at = datetime.fromisoformat(blacklist_data["expires_at"])
# 如果黑名单条目已过期,删除它
if datetime.utcnow() > expires_at:
del self.blacklist_db[token_hash]
return False
return True
def invalidate_token(self, token: str):
"""使单个 Token 失效"""
with self.lock:
token_hash = self._hash_token(token)
# 添加到黑名单
self.add_to_blacklist(token)
# 标记为不活跃
if token_hash in self.token_db:
cache_data = self.token_db[token_hash]
if self.cache.exists(f"token:{token_hash}"):
cache_data = self.cache.get(f"token:{token_hash}")
cache_data["is_active"] = False
self.token_db[token_hash] = cache_data
self.cache.set(f"token:{token_hash}", cache_data)
def invalidate_user_tokens(self, user_id: str, token_type: Optional[str] = None):
"""使用户的所有 Token 失效"""
with self.lock:
if token_type:
keys = [f"{user_id}:{token_type}"]
keys = [f"user:{user_id}:{token_type}"]
else:
keys = [f"{user_id}:access", f"{user_id}:refresh"]
keys = [f"user:{user_id}:access", f"{user_id}:refresh"]
for key in keys:
token_hashes = self.user_sessions_db.get(key, [])
token_hashes = self.cache.get(key, [])
for token_hash in token_hashes:
# 添加到黑名单
# 注意:这里我们只有 token_hash,没有原始 token
# 所以只能标记为不活跃
if token_hash in self.token_db:
cache_data = self.token_db[token_hash]
if self.cache.exists(f"token:{token_hash}"):
cache_data = self.cache.get(f"token:{token_hash}")
cache_data["is_active"] = False
self.token_db[token_hash] = cache_data
self.cache.set(f"token:{token_hash}", cache_data)
# 清空用户会话索引
self.user_sessions_db[key] = []
self.cache.set(key, [])
def get_user_sessions(self, user_id: str) -> List[Dict[str, Any]]:
"""获取用户的所有活跃会话"""
@@ -369,12 +300,12 @@ class JWTCache:
sessions = []
for token_type in ["access", "refresh"]:
key = f"{user_id}:{token_type}"
token_hashes = self.user_sessions_db.get(key, [])
key = f"user:{user_id}:{token_type}"
token_hashes = self.cache.get(key, [])
for token_hash in token_hashes:
if token_hash in self.token_db:
cache_data = self.token_db[token_hash]
if self.cache.exists(f"token:{token_hash}"):
cache_data = self.cache.get(f"token:{token_hash}")
# 检查是否活跃且未过期
if cache_data.get("is_active", True):
@@ -403,28 +334,14 @@ class JWTCache:
current_time = datetime.utcnow()
# 清理过期 Token
for token_hash, data in list(self.token_db.items()):
expires_at = datetime.fromisoformat(data["expires_at"])
for token_hash in self.cache.keys("token:*"):
expires_at = datetime.fromisoformat(self.cache.get(token_hash))
if current_time > expires_at:
del self.token_db[token_hash]
cleaned_count += 1
# 清理过期黑名单
for token_hash, data in list(self.blacklist_db.items()):
expires_at = datetime.fromisoformat(data["expires_at"])
if current_time > expires_at:
del self.blacklist_db[token_hash]
self.cache.delete(token_hash)
cleaned_count += 1
return cleaned_count
def close(self):
with self.lock:
"""关闭所有数据库连接"""
self.token_db.close()
self.blacklist_db.close()
self.user_sessions_db.close()
# 全局缓存实例
_jwt_cache = None
+29 -17
View File
@@ -1,30 +1,43 @@
import logging
import datetime
from pathlib import Path
from .config import Logging
from logging.handlers import RotatingFileHandler
from ..core.config import get_config
config = get_config()
env = config["default"]["env"]
server_config = config[env]["server"]
log_config = config[env]["logging"]
class ExcludeWatchFilesFilter(logging.Filter):
def __init__(self):
super().__init__()
self._filter_starts_with_tulpe = (
"watchfiles",
"aiosqlite",
"apscheduler",
"sqlalchemy",
"sqlitedict",
)
# 重写过滤方法
def filter(self, record):
# 如果日志记录来自 watchfiles 模块,则不记录(返回 False)
return not record.name.startswith("watchfiles")
return not record.name.startswith(self._filter_starts_with_tulpe)
def setup_logging(log_config: Logging):
def setup_logging(logger: logging.Logger, log_name: str = "app"):
log_level = getattr(logging, log_config["level"], logging.INFO)
log_format = log_config["format"]
# 创建文件夹
log_file_path = Path(log_config["file"])
log_file_dir = log_file_path.parent
if log_file_dir != Path("."):
log_file_dir.mkdir(parents=True, exist_ok=True)
# 设置特定日志级别的过滤
logging.getLogger("watchfiles").setLevel(logging.WARNING)
if log_file_path != Path("."):
log_file_path.mkdir(parents=True, exist_ok=True)
# 配置根日志记录器
root_logger = logging.getLogger()
root_logger.setLevel(log_level)
logger.setLevel(log_level)
# 创建格式化器
formatter = logging.Formatter(log_format)
@@ -34,7 +47,7 @@ def setup_logging(log_config: Logging):
backup_count = log_config.get("backup_count", 5) # 默认保留 5 个备份
file_handler = RotatingFileHandler(
log_config["file"],
f"{log_config['file']}/{log_name}-{datetime.date.today()}.log",
encoding="utf-8",
maxBytes=max_bytes,
backupCount=backup_count,
@@ -48,8 +61,8 @@ def setup_logging(log_config: Logging):
console_handler.addFilter(ExcludeWatchFilesFilter())
# 添加处理器到根日志记录器
root_logger.addHandler(file_handler)
root_logger.addHandler(console_handler)
logger.addHandler(file_handler)
logger.addHandler(console_handler)
print(
f"日志系统已配置 - 级别: {logging.getLevelName(log_level)}, 文件: {log_config['file']} "
@@ -59,6 +72,5 @@ def setup_logging(log_config: Logging):
def get_logger(name: str) -> logging.Logger:
logger = logging.getLogger(name)
# 为每个logger添加过滤器
logger.addFilter(ExcludeWatchFilesFilter())
setup_logging(logger, name)
return logger
+281
View File
@@ -0,0 +1,281 @@
"""
APScheduler 集成模块
提供持久化任务调度服务
"""
import os
import sys
import tempfile
from contextlib import suppress
from typing import Dict, List, Optional, Callable, AsyncGenerator, TypedDict, Any
from datetime import datetime
from fastapi import Request
from apscheduler.schedulers.asyncio import AsyncIOScheduler
from apscheduler.jobstores.sqlalchemy import SQLAlchemyJobStore
from apscheduler.executors.asyncio import AsyncIOExecutor
from apscheduler.job import Job
from apscheduler.triggers.interval import IntervalTrigger
from apscheduler.triggers.cron import CronTrigger
from apscheduler.triggers.date import DateTrigger
from ..core.config import get_config
from ..core.logger import get_logger
config = get_config()
logger = get_logger("scheduler")
def acquire_scheduler_lock():
lock_path = os.path.join(tempfile.gettempdir(), "chongming_scheduler.lock")
lock_file = open(lock_path, "w")
try:
if sys.platform == "win32":
import msvcrt
msvcrt.locking(lock_file.fileno(), msvcrt.LK_NBLCK, 1)
else:
import fcntl
fcntl.flock(lock_file.fileno(), fcntl.LOCK_NB | fcntl.LOCK_EX)
return lock_file, True
except (IOError, OSError):
lock_file.close()
return None, False
class TaskService:
"""任务调度服务"""
def __init__(self):
# 使用独立的 SQLite 数据库存储任务(与业务数据库分开)
self.job_store_db_path = config.get("scheduler", {}).get(
"job_store_path", "scheduler_jobs.db"
)
job_store_url = f"sqlite:///{self.job_store_db_path}"
# 配置 job stores 和 executors
jobstores = {"default": SQLAlchemyJobStore(url=job_store_url)}
executors = {"default": AsyncIOExecutor()}
job_defaults = {
"coalesce": True, # 合并错过的任务
"max_instances": 3, # 最大并发实例数
"misfire_grace_time": 30, # 错过任务的宽限时间(秒)
}
self.scheduler = AsyncIOScheduler(
jobstores=jobstores,
executors=executors,
job_defaults=job_defaults,
timezone="Asia/Shanghai",
)
self._started = False
self.lock_file, self.is_scheduler_worker = acquire_scheduler_lock()
async def start(self, init_tasks_callback: Optional[Callable] = None):
"""启动调度器"""
if self.is_scheduler_worker:
if not self._started:
self.scheduler.start()
self._started = True
logger.info("任务调度器已启动")
if init_tasks_callback:
await init_tasks_callback(self)
async def shutdown(self, wait: bool = True):
"""关闭调度器"""
if self.lock_file:
with suppress(Exception):
for job in await self.get_jobs():
await self.pause_job(job.get("id", ""))
if self._started:
self.scheduler.shutdown(wait=wait)
self._started = False
logger.info("任务调度器已关闭")
if sys.platform == "win32":
import msvcrt
msvcrt.locking(self.lock_file.fileno(), msvcrt.LK_UNLCK, 1)
else:
import fcntl
fcntl.flock(lock_file.fileno(), fcntl.LOCK_UN)
self.lock_file.close()
async def add_interval_job(
self,
func: Callable,
seconds: int = 0,
minutes: int = 0,
hours: int = 0,
days: int = 0,
job_id: Optional[str] = None,
args: Optional[List] = None,
kwargs: Optional[Dict] = None,
replace_existing: bool = True,
) -> Job:
"""添加间隔执行任务"""
trigger = IntervalTrigger(
seconds=seconds, minutes=minutes, hours=hours, days=days
)
job = self.scheduler.add_job(
func,
trigger=trigger,
id=job_id,
args=args or [],
kwargs=kwargs or {},
replace_existing=replace_existing,
)
logger.info(f"添加间隔任务: {job.id}")
return job
async def add_cron_job(
self,
func: Callable,
job_id: Optional[str] = None,
year: Optional[int] = None,
month: Optional[int] = None,
day: Optional[int] = None,
week: Optional[int] = None,
day_of_week: Optional[int] = None,
hour: Optional[int] = None,
minute: Optional[int] = None,
second: Optional[int] = None,
args: Optional[List] = None,
kwargs: Optional[Dict] = None,
replace_existing: bool = True,
) -> Job:
"""添加 cron 表达式任务"""
trigger = CronTrigger(
year=year,
month=month,
day=day,
week=week,
day_of_week=day_of_week,
hour=hour,
minute=minute,
second=second,
)
job = self.scheduler.add_job(
func,
trigger=trigger,
id=job_id,
args=args or [],
kwargs=kwargs or {},
replace_existing=replace_existing,
)
logger.info(f"添加 cron 任务: {job.id}")
return job
async def add_date_job(
self,
func: Callable,
run_date: datetime,
job_id: Optional[str] = None,
args: Optional[List] = None,
kwargs: Optional[Dict] = None,
) -> Job:
"""添加一次性任务"""
trigger = DateTrigger(run_date=run_date)
job = self.scheduler.add_job(
func, trigger=trigger, id=job_id, args=args or [], kwargs=kwargs or {}
)
logger.info(f"添加一次性任务: {job.id}")
return job
async def remove_job(self, job_id: str) -> bool:
"""删除任务"""
try:
self.scheduler.remove_job(job_id)
logger.info(f"删除任务: {job_id}")
return True
except Exception as e:
logger.error(f"删除任务失败 {job_id}: {e}")
return False
async def pause_job(self, job_id: str) -> bool:
"""暂停任务"""
try:
self.scheduler.pause_job(job_id)
logger.info(f"暂停任务: {job_id}")
return True
except Exception as e:
logger.error(f"暂停任务失败 {job_id}: {e}")
return False
async def resume_job(self, job_id: str) -> bool:
"""恢复任务"""
try:
self.scheduler.resume_job(job_id)
logger.info(f"恢复任务: {job_id}")
return True
except Exception as e:
logger.error(f"恢复任务失败 {job_id}: {e}")
return False
async def get_job(self, job_id: str) -> Optional[Dict]:
"""获取任务信息"""
job = self.scheduler.get_job(job_id)
if job:
return self._job_to_dict(job)
return None
async def get_jobs(self) -> List[Dict]:
"""获取所有任务"""
jobs = self.scheduler.get_jobs()
return [self._job_to_dict(job) for job in jobs]
async def reschedule_job(
self, job_id: str, trigger_type: str, **trigger_args
) -> Optional[Job]:
"""重新调度任务(修改触发时间)"""
try:
job = self.scheduler.get_job(job_id)
if not job:
return None
if trigger_type == "interval":
trigger = IntervalTrigger(**trigger_args)
elif trigger_type == "cron":
trigger = CronTrigger(**trigger_args)
elif trigger_type == "date":
trigger = DateTrigger(**trigger_args)
else:
raise ValueError(f"不支持的 trigger 类型: {trigger_type}")
job.reschedule(trigger=trigger)
logger.info(f"重新调度任务: {job_id}")
return job
except Exception as e:
logger.error(f"重新调度任务失败 {job_id}: {e}")
return None
def _job_to_dict(self, job: Job) -> Dict:
"""将 Job 对象转换为字典"""
return {
"id": job.id,
"name": job.name,
"next_run_time": (
job.next_run_time.isoformat() if job.next_run_time else None
),
"trigger": str(job.trigger),
"pending": job.pending,
"args": job.args,
"kwargs": job.kwargs,
}
_task_service: Optional[TaskService] = None
def get_task_service_instance():
global _task_service
if _task_service is None:
_task_service = TaskService()
return _task_service
async def get_task_service(request: Request) -> AsyncGenerator:
return request.app.state.task_service
+1
View File
@@ -1,4 +1,5 @@
from typing import Optional
from sqlmodel import SQLModel, Field
from pydantic import BaseModel
+2 -2
View File
@@ -8,9 +8,9 @@ from passlib.context import CryptContext
from sqlalchemy.ext.asyncio import AsyncSession
from ..model.user import User
from .user import UserService
from ..core.jwt_cache import get_jwt_cache, TokenResponse, TokenData
from ..core.config import get_config
from .user import UserService
class AuthService:
@@ -105,7 +105,7 @@ class AuthService:
request: Optional[Request] = None,
) -> Optional[str]:
"""使用刷新令牌获取新的访问令牌"""
validation = self.jwt_cache.validate_token(refresh_token, token_type="access")
validation = self.jwt_cache.validate_token(refresh_token, token_type="refresh")
if not validation:
return None
+2 -1
View File
@@ -1,6 +1,7 @@
from typing import Optional
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from typing import Optional
from ..model.user import User
from ..core.security import get_password_hash
+20
View File
@@ -0,0 +1,20 @@
from typing import Callable, Optional
from ..core.scheduler import TaskService
from .execute_background import execute_background_task
task_dict = {"execute_background_task": execute_background_task}
def get_task(task_name: str) -> Optional[Callable]:
return task_dict.get(task_name, None)
async def init_tasks_callback(task_service: TaskService):
await task_service.add_interval_job(
execute_background_task,
seconds=1,
args=["task_name"],
job_id="task_name",
)
@@ -0,0 +1,8 @@
from ..core.scheduler import logger
async def execute_background_task(task_name: str):
from datetime import datetime
"""模拟后台任务执行"""
logger.info(f"Executing task: {task_name} at {datetime.utcnow()}")
+2 -40
View File
@@ -8,46 +8,6 @@ import sys
import asyncio
def create_admin():
try:
cmd = [
sys.executable,
"../src/chongming/scripts/create_admin.py",
]
print(f"正在运行 create_admin.py: {' '.join(cmd)}")
# 创建包含 UTF-8 编码的环境变量
env = os.environ.copy()
env["PYTHONIOENCODING"] = "utf-8"
env["PYTHONUTF8"] = "1"
result = subprocess.run(
cmd,
cwd="./build",
capture_output=True,
text=True,
encoding="utf-8", # 明确指定编码
errors="replace", # 替换无法解码的字符
env=env, # 传递修改后的环境变量
)
if result.returncode == 0:
print("创建管理员用户成功")
print(result.stdout)
else:
print("创建管理员用户失败")
# 安全地打印错误信息,避免编码问题
try:
print("错误信息:", result.stderr)
except UnicodeEncodeError:
print(
"错误信息:", result.stderr.encode("ascii", "ignore").decode("ascii")
)
except Exception as e:
print("创建管理员用户失败")
print(f"异常: {e}")
def run_pyarmor_obfuscate():
# pyarmor obfuscate --output=obfuscated_script.py original_script.py
obfuscate_files = [
@@ -113,6 +73,8 @@ def run_pyinstaller():
"--hidden-import",
"aiosqlite",
"--hidden-import",
"apscheduler",
"--hidden-import",
"pyjwt",
"--hidden-import",
"sqlitedict",
Generated
+81 -27
View File
@@ -101,6 +101,33 @@ wheels = [
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" },
]
[[package]]
name = "apscheduler"
version = "3.11.2"
source = { registry = "https://mirrors.ustc.edu.cn/pypi/web/simple/" }
dependencies = [
{ name = "backports-zoneinfo", marker = "python_full_version < '3.9'" },
{ name = "tzlocal", version = "5.2", source = { registry = "https://mirrors.ustc.edu.cn/pypi/web/simple/" }, marker = "python_full_version < '3.9'" },
{ name = "tzlocal", version = "5.3.1", source = { registry = "https://mirrors.ustc.edu.cn/pypi/web/simple/" }, marker = "python_full_version >= '3.9'" },
]
sdist = { url = "https://mirrors.ustc.edu.cn/pypi/packages/07/12/3e4389e5920b4c1763390c6d371162f3784f86f85cd6d6c1bfe68eef14e2/apscheduler-3.11.2.tar.gz", hash = "sha256:2a9966b052ec805f020c8c4c3ae6e6a06e24b1bf19f2e11d91d8cca0473eef41", size = 108683, upload-time = "2025-12-22T00:39:34.884Z" }
wheels = [
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/9f/64/2e54428beba8d9992aa478bb8f6de9e4ecaa5f8f513bcfd567ed7fb0262d/apscheduler-3.11.2-py3-none-any.whl", hash = "sha256:ce005177f741409db4e4dd40a7431b76feb856b9dd69d57e0da49d6715bfd26d", size = 64439, upload-time = "2025-12-22T00:39:33.303Z" },
]
[[package]]
name = "backports-zoneinfo"
version = "0.2.1"
source = { registry = "https://mirrors.ustc.edu.cn/pypi/web/simple/" }
sdist = { url = "https://mirrors.ustc.edu.cn/pypi/packages/ad/85/475e514c3140937cf435954f78dedea1861aeab7662d11de232bdaa90655/backports.zoneinfo-0.2.1.tar.gz", hash = "sha256:fadbfe37f74051d024037f223b8e001611eac868b5c5b06144ef4d8b799862f2", size = 74098, upload-time = "2020-06-23T13:51:22.041Z" }
wheels = [
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/4a/6d/eca004eeadcbf8bd64cc96feb9e355536147f0577420b44d80c7cac70767/backports.zoneinfo-0.2.1-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:8961c0f32cd0336fb8e8ead11a1f8cd99ec07145ec2931122faaac1c8f7fd987", size = 35816, upload-time = "2020-06-23T13:51:21.244Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/c1/8f/9b1b920a6a95652463143943fa3b8c000cb0b932ab463764a6f2a2416560/backports.zoneinfo-0.2.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e81b76cace8eda1fca50e345242ba977f9be6ae3945af8d46326d776b4cf78d1", size = 72147, upload-time = "2020-06-23T13:51:17.562Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/1a/ab/3e941e3fcf1b7d3ab3d0233194d99d6a0ed6b24f8f956fc81e47edc8c079/backports.zoneinfo-0.2.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7b0a64cda4145548fed9efc10322770f929b944ce5cee6c0dfe0c87bf4c0c8c9", size = 74033, upload-time = "2020-06-23T13:51:14.592Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/c0/34/5fdb0a3a28841d215c255be8fc60b8666257bb6632193c86fd04b63d4a31/backports.zoneinfo-0.2.1-cp38-cp38-win32.whl", hash = "sha256:1b13e654a55cd45672cb54ed12148cd33628f672548f373963b0bff67b217328", size = 36803, upload-time = "2020-06-23T13:51:07.517Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/78/cc/e27fd6493bbce8dbea7e6c1bc861fe3d3bc22c4f7c81f4c3befb8ff5bfaf/backports.zoneinfo-0.2.1-cp38-cp38-win_amd64.whl", hash = "sha256:4a0f800587060bf8880f954dbef70de6c11bbe59c673c3d818921f042f9954a6", size = 38967, upload-time = "2020-06-23T13:51:13.735Z" },
]
[[package]]
name = "bcrypt"
version = "4.0.1"
@@ -312,7 +339,9 @@ source = { editable = "." }
dependencies = [
{ name = "aiosqlite", version = "0.20.0", source = { registry = "https://mirrors.ustc.edu.cn/pypi/web/simple/" }, marker = "python_full_version < '3.9'" },
{ name = "aiosqlite", version = "0.22.1", source = { registry = "https://mirrors.ustc.edu.cn/pypi/web/simple/" }, marker = "python_full_version >= '3.9'" },
{ name = "apscheduler" },
{ name = "bcrypt" },
{ name = "diskcache" },
{ name = "fastapi", version = "0.124.4", source = { registry = "https://mirrors.ustc.edu.cn/pypi/web/simple/" }, marker = "python_full_version < '3.9'" },
{ name = "fastapi", version = "0.128.8", source = { registry = "https://mirrors.ustc.edu.cn/pypi/web/simple/" }, marker = "python_full_version == '3.9.*'" },
{ name = "fastapi", version = "0.135.1", source = { registry = "https://mirrors.ustc.edu.cn/pypi/web/simple/" }, marker = "python_full_version >= '3.10'" },
@@ -321,7 +350,6 @@ dependencies = [
{ name = "pyjwt", version = "2.9.0", source = { registry = "https://mirrors.ustc.edu.cn/pypi/web/simple/" }, marker = "python_full_version < '3.9'" },
{ name = "pyjwt", version = "2.11.0", source = { registry = "https://mirrors.ustc.edu.cn/pypi/web/simple/" }, marker = "python_full_version >= '3.9'" },
{ name = "sqlite-vfs" },
{ name = "sqlitedict" },
{ name = "sqlmodel", version = "0.0.29", source = { registry = "https://mirrors.ustc.edu.cn/pypi/web/simple/" }, marker = "python_full_version < '3.9'" },
{ name = "sqlmodel", version = "0.0.34", source = { registry = "https://mirrors.ustc.edu.cn/pypi/web/simple/" }, marker = "python_full_version == '3.9.*'" },
{ name = "sqlmodel", version = "0.0.37", source = { registry = "https://mirrors.ustc.edu.cn/pypi/web/simple/" }, marker = "python_full_version >= '3.10'" },
@@ -342,13 +370,14 @@ dev = [
[package.metadata]
requires-dist = [
{ name = "aiosqlite" },
{ name = "apscheduler" },
{ name = "bcrypt", specifier = "==4.0.1" },
{ name = "diskcache", specifier = ">=5.6.3" },
{ name = "fastapi", extras = ["uvicorn"] },
{ name = "module-bank" },
{ name = "passlib", extras = ["bcrypt"] },
{ name = "pyjwt" },
{ name = "sqlite-vfs" },
{ name = "sqlitedict" },
{ name = "sqlmodel", extras = ["aiosqlite"] },
{ name = "tomli" },
{ name = "typing-extensions" },
@@ -463,6 +492,15 @@ wheels = [
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/bc/58/6b3d24e6b9bc474a2dcdee65dfd1f008867015408a271562e4b690561a4d/cryptography-46.0.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:8456928655f856c6e1533ff59d5be76578a7157224dbd9ce6872f25055ab9ab7", size = 3407605, upload-time = "2026-02-10T19:18:29.233Z" },
]
[[package]]
name = "diskcache"
version = "5.6.3"
source = { registry = "https://mirrors.ustc.edu.cn/pypi/web/simple/" }
sdist = { url = "https://mirrors.ustc.edu.cn/pypi/packages/3f/21/1c1ffc1a039ddcc459db43cc108658f32c57d271d7289a2794e401d0fdb6/diskcache-5.6.3.tar.gz", hash = "sha256:2c3a3fa2743d8535d832ec61c2054a1641f41775aa7c556758a109941e33e4fc", size = 67916, upload-time = "2023-08-31T06:12:00.316Z" }
wheels = [
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/3f/27/4570e78fc0bf5ea0ca45eb1de3818a23787af9b390c0b0a0033a1b8236f9/diskcache-5.6.3-py3-none-any.whl", hash = "sha256:5e31b2d5fbad117cc363ebaf6b689474db18a1f6438bc82358b024abd4c2ca19", size = 45550, upload-time = "2023-08-31T06:11:58.822Z" },
]
[[package]]
name = "exceptiongroup"
version = "1.3.1"
@@ -544,7 +582,6 @@ wheels = [
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/25/90/5234a78dc0ef6496a6eb97b67a42a8e96742a56f7dc808cb954a85390448/greenlet-3.1.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:0bbae94a29c9e5c7e4a2b7f0aae5c17e8e90acbfd3bf6270eeba60c39fce3563", size = 271235, upload-time = "2024-09-20T17:07:18.761Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/7c/16/cd631fa0ab7d06ef06387135b7549fdcc77d8d859ed770a0d28e47b20972/greenlet-3.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fde093fb93f35ca72a556cf72c92ea3ebfda3d79fc35bb19fbe685853869a83", size = 637168, upload-time = "2024-09-20T17:36:43.774Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/2f/b1/aed39043a6fec33c284a2c9abd63ce191f4f1a07319340ffc04d2ed3256f/greenlet-3.1.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:36b89d13c49216cadb828db8dfa6ce86bbbc476a82d3a6c397f0efae0525bdd0", size = 648826, upload-time = "2024-09-20T17:39:16.921Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/76/25/40e0112f7f3ebe54e8e8ed91b2b9f970805143efef16d043dfc15e70f44b/greenlet-3.1.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:94b6150a85e1b33b40b1464a3f9988dcc5251d6ed06842abff82e42632fac120", size = 644443, upload-time = "2024-09-20T17:44:21.896Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/fb/2f/3850b867a9af519794784a7eeed1dd5bc68ffbcc5b28cef703711025fd0a/greenlet-3.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93147c513fac16385d1036b7e5b102c7fbbdb163d556b791f0f11eada7ba65dc", size = 643295, upload-time = "2024-09-20T17:08:37.951Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/cf/69/79e4d63b9387b48939096e25115b8af7cd8a90397a304f92436bcb21f5b2/greenlet-3.1.1-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:da7a9bff22ce038e19bf62c4dd1ec8391062878710ded0a845bcf47cc0200617", size = 599544, upload-time = "2024-09-20T17:08:27.894Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/46/1d/44dbcb0e6c323bd6f71b8c2f4233766a5faf4b8948873225d34a0b7efa71/greenlet-3.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b2795058c23988728eec1f36a4e5e4ebad22f8320c85f3587b539b9ac84128d7", size = 1125456, upload-time = "2024-09-20T17:44:11.755Z" },
@@ -553,7 +590,6 @@ wheels = [
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/28/62/1c2665558618553c42922ed47a4e6d6527e2fa3516a8256c2f431c5d0441/greenlet-3.1.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:e4d333e558953648ca09d64f13e6d8f0523fa705f51cae3f03b5983489958c70", size = 272479, upload-time = "2024-09-20T17:07:22.332Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/76/9d/421e2d5f07285b6e4e3a676b016ca781f63cfe4a0cd8eaecf3fd6f7a71ae/greenlet-3.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09fc016b73c94e98e29af67ab7b9a879c307c6731a2c9da0db5a7d9b7edd1159", size = 640404, upload-time = "2024-09-20T17:36:45.588Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/e5/de/6e05f5c59262a584e502dd3d261bbdd2c97ab5416cc9c0b91ea38932a901/greenlet-3.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d5e975ca70269d66d17dd995dafc06f1b06e8cb1ec1e9ed54c1d1e4a7c4cf26e", size = 652813, upload-time = "2024-09-20T17:39:19.052Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/49/93/d5f93c84241acdea15a8fd329362c2c71c79e1a507c3f142a5d67ea435ae/greenlet-3.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b2813dc3de8c1ee3f924e4d4227999285fd335d1bcc0d2be6dc3f1f6a318ec1", size = 648517, upload-time = "2024-09-20T17:44:24.101Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/15/85/72f77fc02d00470c86a5c982b8daafdf65d38aefbbe441cebff3bf7037fc/greenlet-3.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e347b3bfcf985a05e8c0b7d462ba6f15b1ee1c909e2dcad795e49e91b152c383", size = 647831, upload-time = "2024-09-20T17:08:40.577Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/f7/4b/1c9695aa24f808e156c8f4813f685d975ca73c000c2a5056c514c64980f6/greenlet-3.1.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e8f8c9cb53cdac7ba9793c276acd90168f416b9ce36799b9b885790f8ad6c0a", size = 602413, upload-time = "2024-09-20T17:08:31.728Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/76/70/ad6e5b31ef330f03b12559d19fda2606a522d3849cde46b24f223d6d1619/greenlet-3.1.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:62ee94988d6b4722ce0028644418d93a52429e977d742ca2ccbe1c4f4a792511", size = 1129619, upload-time = "2024-09-20T17:44:14.222Z" },
@@ -562,7 +598,6 @@ wheels = [
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/7d/ec/bad1ac26764d26aa1353216fcbfa4670050f66d445448aafa227f8b16e80/greenlet-3.1.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:4afe7ea89de619adc868e087b4d2359282058479d7cfb94970adf4b55284574d", size = 274260, upload-time = "2024-09-20T17:08:07.301Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/66/d4/c8c04958870f482459ab5956c2942c4ec35cac7fe245527f1039837c17a9/greenlet-3.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f406b22b7c9a9b4f8aa9d2ab13d6ae0ac3e85c9a809bd590ad53fed2bf70dc79", size = 649064, upload-time = "2024-09-20T17:36:47.628Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/51/41/467b12a8c7c1303d20abcca145db2be4e6cd50a951fa30af48b6ec607581/greenlet-3.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c3a701fe5a9695b238503ce5bbe8218e03c3bcccf7e204e455e7462d770268aa", size = 663420, upload-time = "2024-09-20T17:39:21.258Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/27/8f/2a93cd9b1e7107d5c7b3b7816eeadcac2ebcaf6d6513df9abaf0334777f6/greenlet-3.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2846930c65b47d70b9d178e89c7e1a69c95c1f68ea5aa0a58646b7a96df12441", size = 658035, upload-time = "2024-09-20T17:44:26.501Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/57/5c/7c6f50cb12be092e1dccb2599be5a942c3416dbcfb76efcf54b3f8be4d8d/greenlet-3.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99cfaa2110534e2cf3ba31a7abcac9d328d1d9f1b95beede58294a60348fba36", size = 660105, upload-time = "2024-09-20T17:08:42.048Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/f1/66/033e58a50fd9ec9df00a8671c74f1f3a320564c6415a4ed82a1c651654ba/greenlet-3.1.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1443279c19fca463fc33e65ef2a935a5b09bb90f978beab37729e1c3c6c25fe9", size = 613077, upload-time = "2024-09-20T17:08:33.707Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/19/c5/36384a06f748044d06bdd8776e231fadf92fc896bd12cb1c9f5a1bda9578/greenlet-3.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b7cede291382a78f7bb5f04a529cb18e068dd29e0fb27376074b6d0317bf4dd0", size = 1135975, upload-time = "2024-09-20T17:44:15.989Z" },
@@ -571,7 +606,6 @@ wheels = [
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/f3/57/0db4940cd7bb461365ca8d6fd53e68254c9dbbcc2b452e69d0d41f10a85e/greenlet-3.1.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:05175c27cb459dcfc05d026c4232f9de8913ed006d42713cb8a5137bd49375f1", size = 272990, upload-time = "2024-09-20T17:08:26.312Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/1c/ec/423d113c9f74e5e402e175b157203e9102feeb7088cee844d735b28ef963/greenlet-3.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:935e943ec47c4afab8965954bf49bfa639c05d4ccf9ef6e924188f762145c0ff", size = 649175, upload-time = "2024-09-20T17:36:48.983Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/a9/46/ddbd2db9ff209186b7b7c621d1432e2f21714adc988703dbdd0e65155c77/greenlet-3.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:667a9706c970cb552ede35aee17339a18e8f2a87a51fba2ed39ceeeb1004798a", size = 663425, upload-time = "2024-09-20T17:39:22.705Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/bc/f9/9c82d6b2b04aa37e38e74f0c429aece5eeb02bab6e3b98e7db89b23d94c6/greenlet-3.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b8a678974d1f3aa55f6cc34dc480169d58f2e6d8958895d68845fa4ab566509e", size = 657736, upload-time = "2024-09-20T17:44:28.544Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/d9/42/b87bc2a81e3a62c3de2b0d550bf91a86939442b7ff85abb94eec3fc0e6aa/greenlet-3.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efc0f674aa41b92da8c49e0346318c6075d734994c3c4e4430b1c3f853e498e4", size = 660347, upload-time = "2024-09-20T17:08:45.56Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/37/fa/71599c3fd06336cdc3eac52e6871cfebab4d9d70674a9a9e7a482c318e99/greenlet-3.1.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0153404a4bb921f0ff1abeb5ce8a5131da56b953eda6e14b88dc6bbc04d2049e", size = 615583, upload-time = "2024-09-20T17:08:36.85Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/4e/96/e9ef85de031703ee7a4483489b40cf307f93c1824a02e903106f2ea315fe/greenlet-3.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:275f72decf9932639c1c6dd1013a1bc266438eb32710016a1c742df5da6e60a1", size = 1133039, upload-time = "2024-09-20T17:44:18.287Z" },
@@ -579,7 +613,6 @@ wheels = [
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/1f/1b/54336d876186920e185066d8c3024ad55f21d7cc3683c856127ddb7b13ce/greenlet-3.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:b42703b1cf69f2aa1df7d1030b9d77d3e584a70755674d60e710f0af570f3761", size = 299490, upload-time = "2024-09-20T17:17:09.501Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/5f/17/bea55bf36990e1638a2af5ba10c1640273ef20f627962cf97107f1e5d637/greenlet-3.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f1695e76146579f8c06c1509c7ce4dfe0706f49c6831a817ac04eebb2fd02011", size = 643731, upload-time = "2024-09-20T17:36:50.376Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/78/d2/aa3d2157f9ab742a08e0fd8f77d4699f37c22adfbfeb0c610a186b5f75e0/greenlet-3.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7876452af029456b3f3549b696bb36a06db7c90747740c5302f74a9e9fa14b13", size = 649304, upload-time = "2024-09-20T17:39:24.55Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/f1/8e/d0aeffe69e53ccff5a28fa86f07ad1d2d2d6537a9506229431a2a02e2f15/greenlet-3.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4ead44c85f8ab905852d3de8d86f6f8baf77109f9da589cb4fa142bd3b57b475", size = 646537, upload-time = "2024-09-20T17:44:31.102Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/05/79/e15408220bbb989469c8871062c97c6c9136770657ba779711b90870d867/greenlet-3.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8320f64b777d00dd7ccdade271eaf0cad6636343293a25074cc5566160e4de7b", size = 642506, upload-time = "2024-09-20T17:08:47.852Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/18/87/470e01a940307796f1d25f8167b551a968540fbe0551c0ebb853cb527dd6/greenlet-3.1.1-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6510bf84a6b643dabba74d3049ead221257603a253d0a9873f55f6a59a65f822", size = 602753, upload-time = "2024-09-20T17:08:38.079Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/e2/72/576815ba674eddc3c25028238f74d7b8068902b3968cbe456771b166455e/greenlet-3.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:04b013dc07c96f83134b1e99888e7a79979f1a247e2a9f59697fa14b5862ed01", size = 1122731, upload-time = "2024-09-20T17:44:20.556Z" },
@@ -587,7 +620,6 @@ wheels = [
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/97/83/bdf5f69fcf304065ec7cf8fc7c08248479cfed9bcca02bf0001c07e000aa/greenlet-3.1.1-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:346bed03fe47414091be4ad44786d1bd8bef0c3fcad6ed3dee074a032ab408a9", size = 271017, upload-time = "2024-09-20T17:08:54.806Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/31/4a/2d4443adcb38e1e90e50c653a26b2be39998ea78ca1a4cf414dfdeb2e98b/greenlet-3.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dfc59d69fc48664bc693842bd57acfdd490acafda1ab52c7836e3fc75c90a111", size = 642888, upload-time = "2024-09-20T17:36:53.307Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/5a/c9/b5d9ac1b932aa772dd1eb90a8a2b30dbd7ad5569dcb7fdac543810d206b4/greenlet-3.1.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d21e10da6ec19b457b82636209cbe2331ff4306b54d06fa04b7c138ba18c8a81", size = 655451, upload-time = "2024-09-20T17:39:28.564Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/a8/18/218e21caf7caba5b2236370196eaebc00987d4a2b2d3bf63cc4d4dd5a69f/greenlet-3.1.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:37b9de5a96111fc15418819ab4c4432e4f3c2ede61e660b1e33971eba26ef9ba", size = 651409, upload-time = "2024-09-20T17:44:34.134Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/a7/25/de419a2b22fa6e18ce3b2a5adb01d33ec7b2784530f76fa36ba43d8f0fac/greenlet-3.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ef9ea3f137e5711f0dbe5f9263e8c009b7069d8a1acea822bd5e9dae0ae49c8", size = 650661, upload-time = "2024-09-20T17:08:50.932Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/d8/88/0ce16c0afb2d71d85562a7bcd9b092fec80a7767ab5b5f7e1bbbca8200f8/greenlet-3.1.1-cp38-cp38-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:85f3ff71e2e60bd4b4932a043fbbe0f499e263c628390b285cb599154a3b03b1", size = 605959, upload-time = "2024-09-20T17:08:43.376Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/5a/10/39a417ad0afb0b7e5b150f1582cdeb9416f41f2e1df76018434dfac4a6cc/greenlet-3.1.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:95ffcf719966dd7c453f908e208e14cde192e09fde6c7186c8f1896ef778d8cd", size = 1132341, upload-time = "2024-09-20T17:44:25.225Z" },
@@ -597,7 +629,6 @@ wheels = [
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/8c/82/8051e82af6d6b5150aacb6789a657a8afd48f0a44d8e91cb72aaaf28553a/greenlet-3.1.1-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:396979749bd95f018296af156201d6211240e7a23090f50a8d5d18c370084dc3", size = 270027, upload-time = "2024-09-20T17:08:27.964Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/f9/74/f66de2785880293780eebd18a2958aeea7cbe7814af1ccef634f4701f846/greenlet-3.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca9d0ff5ad43e785350894d97e13633a66e2b50000e8a183a50a88d834752d42", size = 634822, upload-time = "2024-09-20T17:36:54.764Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/68/23/acd9ca6bc412b02b8aa755e47b16aafbe642dde0ad2f929f836e57a7949c/greenlet-3.1.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f6ff3b14f2df4c41660a7dec01045a045653998784bf8cfcb5a525bdffffbc8f", size = 646866, upload-time = "2024-09-20T17:39:30.2Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/a9/ab/562beaf8a53dc9f6b2459f200e7bc226bb07e51862a66351d8b7817e3efd/greenlet-3.1.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:94ebba31df2aa506d7b14866fed00ac141a867e63143fe5bca82a8e503b36437", size = 641985, upload-time = "2024-09-20T17:44:36.168Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/03/d3/1006543621f16689f6dc75f6bcf06e3c23e044c26fe391c16c253623313e/greenlet-3.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:73aaad12ac0ff500f62cebed98d8789198ea0e6f233421059fa68a5aa7220145", size = 641268, upload-time = "2024-09-20T17:08:52.469Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/2f/c1/ad71ce1b5f61f900593377b3f77b39408bce5dc96754790311b49869e146/greenlet-3.1.1-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63e4844797b975b9af3a3fb8f7866ff08775f5426925e1e0bbcfe7932059a12c", size = 597376, upload-time = "2024-09-20T17:08:46.096Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/f7/ff/183226685b478544d61d74804445589e069d00deb8ddef042699733950c7/greenlet-3.1.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7939aa3ca7d2a1593596e7ac6d59391ff30281ef280d8632fa03d81f7c5f955e", size = 1123359, upload-time = "2024-09-20T17:44:27.559Z" },
@@ -618,7 +649,6 @@ wheels = [
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/ff/d6/b3db928fc329b1b19ba32ffe143d2305f3aaafc583f5e1074c74ec445189/greenlet-3.2.5-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:34cc7cf8ab6f4b85298b01e13e881265ee7b3c1daf6bc10a2944abc15d4f87c3", size = 275803, upload-time = "2026-02-20T20:06:42.541Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/b3/ff/ab0ad4ff3d9e1faa266de4f6c79763b33fccd9265995f2940192494cc0ec/greenlet-3.2.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c11fe0cfb0ce33132f0b5d27eeadd1954976a82e5e9b60909ec2c4b884a55382", size = 633556, upload-time = "2026-02-20T20:30:41.594Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/da/dd/7b3ac77099a1671af8077ecedb12c9a1be1310e4c35bb69fd34c18ab6093/greenlet-3.2.5-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:a145f4b1c4ed7a2c94561b7f18b4beec3d3fb6f0580db22f7ed1d544e0620b34", size = 644943, upload-time = "2026-02-20T20:37:23.084Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/56/f0/bea7e7909ea9045b0c5055dad1ec9b81c82b761b4567e625f4f8349acfa1/greenlet-3.2.5-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:edbf4ab9a7057ee430a678fe2ef37ea5d69125d6bdc7feb42ed8d871c737e63b", size = 640849, upload-time = "2026-02-20T20:43:57.305Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/0f/36/84630e9ff1dfc8b7690957c0f77834a84eabdbd9c4977c3a2d0cbd5325c2/greenlet-3.2.5-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc1d01bdd67db3e5711e6246e451d7a0f75fae7bbf40adde129296a7f9aa7cc9", size = 639841, upload-time = "2026-02-20T20:07:17.473Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/12/c4/6a2ee6c676dea7a05a3c3c1291fbc8ea44f26456b0accc891471293825af/greenlet-3.2.5-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd593db7ee1fa8a513a48a404f8cc4126998a48025e3f5cbbc68d51be0a6bf66", size = 588813, upload-time = "2026-02-20T20:07:56.171Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/01/c0/75e75c2c993aa850292561ec80f5c263e3924e5843aa95a38716df69304c/greenlet-3.2.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ac8db07bced2c39b987bba13a3195f8157b0cfbce54488f86919321444a1cc3c", size = 1117377, upload-time = "2026-02-20T20:32:48.452Z" },
@@ -626,7 +656,6 @@ wheels = [
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/d8/7b/c6e1192c795c0c12871e199237909a6bd35757d92c8472c7c019959b8637/greenlet-3.2.5-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:acabf468466d18017e2ae5fbf1a5a88b86b48983e550e1ae1437b69a83d9f4ac", size = 276916, upload-time = "2026-02-20T20:06:18.166Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/3e/b6/9887b559f3e1952d23052ec352e9977e808a2246c7cb8282a38337221e88/greenlet-3.2.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:472841de62d60f2cafd60edd4fd4dd7253eb70e6eaf14b8990dcaf177f4af957", size = 636107, upload-time = "2026-02-20T20:30:43.362Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/8a/be/e3e48b63bbc27d660fa1d98aecb64906b90a12e686a436169c1330ef34b2/greenlet-3.2.5-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:7d951e7d628a6e8b68af469f0fe4f100ef64c4054abeb9cdafbfaa30a920c950", size = 648240, upload-time = "2026-02-20T20:37:24.608Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/17/f6/2cbe999683f759f14f598234f04ae8ba6f22953a624b3a7a630003e6bfff/greenlet-3.2.5-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:87b791dd0e031a574249af717ac36f7031b18c35329561c1e0368201c18caf1f", size = 644170, upload-time = "2026-02-20T20:43:59.002Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/4c/ac/e731ed62576e91e533b36d0d97325adc2786674ab9e48ed8a6a24f4ef4e9/greenlet-3.2.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8317d732e2ae0935d9ed2af2ea876fa714cf6f3b887a31ca150b54329b0a6e9", size = 643313, upload-time = "2026-02-20T20:07:19.012Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/70/64/99e5cdceb494bd4c1341c45b93f322601d2c8a5e1e4d1c7a2d24c5ed0570/greenlet-3.2.5-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ce8aed6fdd5e07d3cbb988cbdc188266a4eb9e1a52db9ef5c6526e59962d3933", size = 591295, upload-time = "2026-02-20T20:07:57.286Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/ee/e9/968e11f388c2b8792d3b8b40a57984c894a3b4745dae3662dce722653bc5/greenlet-3.2.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:60c06b502d56d5451f60ca665691da29f79ed95e247bcf8ce5024d7bbe64acb9", size = 1120277, upload-time = "2026-02-20T20:32:50.103Z" },
@@ -634,7 +663,6 @@ wheels = [
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/ad/32/022b21523eee713e7550162d5ca6aed23f913cc2c6232b154b9fd9badc07/greenlet-3.2.5-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:2acb30e77042f747ca81f0a10cc153296567e92e666c5e1b117f4595afd43352", size = 278412, upload-time = "2026-02-20T20:03:15.02Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/90/c5/8a3b0ed3cc34d8b988a44349437dfa0941f9c23ac108175f7b4ccea97111/greenlet-3.2.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:393c03c26c865f17f31d8db2f09603fadbe0581ad85a5d5908b131549fc38217", size = 644616, upload-time = "2026-02-20T20:30:44.823Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/b1/2c/2627bea183554695016af6cae93d7474fa90f61e5a6601a84ae7841cb720/greenlet-3.2.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:04e6a202cde56043fd355fefd1552c4caa5c087528121871d950eb4f1b51fa99", size = 658813, upload-time = "2026-02-20T20:37:26.255Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/44/c6/a80fc96f7cca7962dd972875d12c52dfabc94cb02bfeb19f3e7e169fca44/greenlet-3.2.5-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d5583b2ffa677578a384337ee13125bdf9a427485d689014b39d638a4f3d8dbe", size = 653512, upload-time = "2026-02-20T20:44:00.343Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/2f/1b/75a5aeff487a26ba427a3837da6372f1fe6f2a9c6b2898e28ac99d491c11/greenlet-3.2.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:45fcea7b697b91290b36eafc12fff479aca6ba6500d98ef6f34d5634c7119cbe", size = 655426, upload-time = "2026-02-20T20:07:20.124Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/53/91/9b5dfb4f3c88f8247c7a8f4c3759f0740bfa6bb0c59a9f6bf938e913df56/greenlet-3.2.5-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f96e2bb8a56b7e1aed1dbfbbe0050cb2ecca99c7c91892fd1771e3afab63b3e3", size = 611138, upload-time = "2026-02-20T20:07:58.966Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/b4/8d/d0b086410512d9859c84e9242a9b341de9f5566011ddf3a3f6886b842b61/greenlet-3.2.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d7456e67b0be653dfe643bb37d9566cd30939c80f858e2ce6d2d54951f75b14a", size = 1126896, upload-time = "2026-02-20T20:32:52.198Z" },
@@ -642,7 +670,6 @@ wheels = [
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/dd/95/d5d332fb73affaf7a1fbe80e49c2c7eae4f17c645af24a3b3fa25736d6f0/greenlet-3.2.5-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:f2cc88b50b9006b324c1b9f5f3552f9d4564c78af57cdfb4c7baf4f0aa089146", size = 277166, upload-time = "2026-02-20T20:03:57.077Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/6c/77/89458e20db5a4f1c64f9a0191561227e76d809941ca2d7529006d17d3450/greenlet-3.2.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e66872daffa360b2537170b73ad530f14fa31785b1bc78080125d92edf0a6def", size = 644674, upload-time = "2026-02-20T20:30:46.118Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/90/f8/9962175d2f2eaa629a7fd7545abacc8c4deda3baa4e52c1526d2eb5f5546/greenlet-3.2.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:c5445ddb7b586d870dad32ca9fc47c287d6022a528d194efdb8912093c5303ad", size = 658834, upload-time = "2026-02-20T20:37:27.466Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/81/71/52c21a7106ce5218aa6fa59ec32825b2655f875a09b69f68bd3e5d01feb3/greenlet-3.2.5-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:fd904626b8779810062cb455514594776e3cba3b8c0ba4939894df9f7b384971", size = 653091, upload-time = "2026-02-20T20:44:01.927Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/f5/d7/826d0e080f0a7ad5ec47c8d143bbd3ca0887657bb806595fe2434d12938a/greenlet-3.2.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:752c896a8c976548faafe8a306d446c6a4c68d4fd24699b84d4393bd9ac69a8e", size = 655760, upload-time = "2026-02-20T20:07:21.551Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/41/cc/33bd4c2f816be8c8e16f71740c4130adf3a66a3dd2ba29de72b9d8dd1096/greenlet-3.2.5-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:499b809e7738c8af0ff9ac9d5dd821cb93f4293065a9237543217f0b252f950a", size = 614132, upload-time = "2026-02-20T20:08:00.351Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/48/79/f3891dcfc59097474a53cc3c624f2f2465e431ab493bda043b8c873fb20a/greenlet-3.2.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:2c7429f6e9cea7cbf2637d86d3db12806ba970f7f972fcab39d6b54b4457cbaf", size = 1125286, upload-time = "2026-02-20T20:32:54.032Z" },
@@ -650,13 +677,11 @@ wheels = [
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/f6/9d/4e9b941be05f8da7ba804c6413761d2c11cca05994cbf0a015bd729419f0/greenlet-3.2.5-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:7123b29e6bad2f3f89681be4ef316480fca798ebe8d22fbaced9cc3775007a4f", size = 277627, upload-time = "2026-02-20T20:06:04.798Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/23/cb/a73625c9a35138330014ecf3740c0d62e0c2b5e7279bb7f2586b1b199fac/greenlet-3.2.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6e8fe0c72603201a86b2e038daf9b6c8570715f8779566419cff543b6ace88de", size = 690001, upload-time = "2026-02-20T20:30:47.754Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/83/49/6d1531109507bce7dfb23acf57a87013627ed3ac058851176e443a6a9134/greenlet-3.2.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:050703a60603db0e817364d69e048c70af299040c13a7e67792b9e62d4571196", size = 702953, upload-time = "2026-02-20T20:37:29.125Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/90/ac/6d8fff3b273fc60ad4b46f8411fe91c1e4cca064dfff68d096bc982fa6d0/greenlet-3.2.5-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:04633da773ae432649a3f092a8e4add390732cc9e1ab52c8ff2c91b8dc86f202", size = 698353, upload-time = "2026-02-20T20:44:03.547Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/f7/38/f958ee90fab93529b30cc1e4a59b27c1112b640570043a84af84da3b3b98/greenlet-3.2.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6712bfd520530eb67331813f7112d3ee18e206f48b3d026d8a96cd2d2ad20251", size = 698995, upload-time = "2026-02-20T20:07:22.663Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/51/c1/a603906e79716d61f08afedaf8aed62017661457aef233d62d6e57ecd511/greenlet-3.2.5-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bc06a78fa3ffbe2a75f1ebc7e040eacf6fa1050a9432953ab111fbbbf0d03c1", size = 661175, upload-time = "2026-02-20T20:08:01.477Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/3f/8f/f880ff4587d236b4d06893fb34da6b299aa0d00f6c8259673f80e1b6d63c/greenlet-3.2.5-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:dbe0e81e24982bb45907ca20152b31c2e3300ca352fdc4acbd4956e4a2cbc195", size = 274946, upload-time = "2026-02-20T20:05:21.979Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/3c/50/f6c78b8420187fdfe97fcf2e6d1dd243a7742d272c32fd4d4b1095474b37/greenlet-3.2.5-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:15871afc0d78ec87d15d8412b337f287fc69f8f669346e391585824970931c48", size = 631781, upload-time = "2026-02-20T20:30:48.845Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/26/d6/3277f92e1961e6e9f41d9f173ea74b5c1f7065072637669f761626f26cc0/greenlet-3.2.5-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5bf0d7d62e356ef2e87e55e46a4e930ac165f9372760fb983b5631bb479e9d3a", size = 643740, upload-time = "2026-02-20T20:37:30.639Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/f8/8a/c37b87659378759f158dbe03eaeb7ed002a8968f1c649b2972f5323f99b2/greenlet-3.2.5-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:e3f03ddd7142c758ab41c18089a1407b9959bd276b4e6dfbd8fd06403832c87a", size = 639098, upload-time = "2026-02-20T20:44:07.287Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/2a/6a/4f79d2e7b5ef3723fc5ffea0d6cb22627e5f95e0f19c973fa12bf1cf7891/greenlet-3.2.5-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6dff6433742073e5b6ad40953a78a0e8cddcb3f6869e5ea635d29a810ca5e7d0", size = 638382, upload-time = "2026-02-20T20:07:23.883Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/4d/59/7aadf33f23c65dbf4db27e7f5b60c414797a61e954352ae4a86c5c8b0553/greenlet-3.2.5-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bdd67619cefe1cc9fcab57c8853d2bb36eca9f166c0058cc0d428d471f7c785c", size = 587516, upload-time = "2026-02-20T20:08:02.841Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/1d/46/b3422959f830de28a4eea447414e6bd7b980d755892f66ab52ad805da1c4/greenlet-3.2.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:3828b309dfb1f117fe54867512a8265d8d4f00f8de6908eef9b885f4d8789062", size = 1115818, upload-time = "2026-02-20T20:32:55.786Z" },
@@ -675,7 +700,6 @@ wheels = [
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/38/3f/9859f655d11901e7b2996c6e3d33e0caa9a1d4572c3bc61ed0faa64b2f4c/greenlet-3.3.2-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:9bc885b89709d901859cf95179ec9f6bb67a3d2bb1f0e88456461bd4b7f8fd0d", size = 277747, upload-time = "2026-02-20T20:16:21.325Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/fb/07/cb284a8b5c6498dbd7cba35d31380bb123d7dceaa7907f606c8ff5993cbf/greenlet-3.3.2-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b568183cf65b94919be4438dc28416b234b678c608cafac8874dfeeb2a9bbe13", size = 579202, upload-time = "2026-02-20T20:47:28.955Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/ed/45/67922992b3a152f726163b19f890a85129a992f39607a2a53155de3448b8/greenlet-3.3.2-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:527fec58dc9f90efd594b9b700662ed3fb2493c2122067ac9c740d98080a620e", size = 590620, upload-time = "2026-02-20T20:55:55.581Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/03/5f/6e2a7d80c353587751ef3d44bb947f0565ec008a2e0927821c007e96d3a7/greenlet-3.3.2-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:508c7f01f1791fbc8e011bd508f6794cb95397fdb198a46cb6635eb5b78d85a7", size = 602132, upload-time = "2026-02-20T21:02:43.261Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/ad/55/9f1ebb5a825215fadcc0f7d5073f6e79e3007e3282b14b22d6aba7ca6cb8/greenlet-3.3.2-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ad0c8917dd42a819fe77e6bdfcb84e3379c0de956469301d9fd36427a1ca501f", size = 591729, upload-time = "2026-02-20T20:20:58.395Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/24/b4/21f5455773d37f94b866eb3cf5caed88d6cea6dd2c6e1f9c34f463cba3ec/greenlet-3.3.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:97245cc10e5515dbc8c3104b2928f7f02b6813002770cfaffaf9a6e0fc2b94ef", size = 1551946, upload-time = "2026-02-20T20:49:31.102Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/00/68/91f061a926abead128fe1a87f0b453ccf07368666bd59ffa46016627a930/greenlet-3.3.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8c1fdd7d1b309ff0da81d60a9688a8bd044ac4e18b250320a96fc68d31c209ca", size = 1618494, upload-time = "2026-02-20T20:21:06.541Z" },
@@ -683,7 +707,6 @@ wheels = [
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/f3/47/16400cb42d18d7a6bb46f0626852c1718612e35dcb0dffa16bbaffdf5dd2/greenlet-3.3.2-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:c56692189a7d1c7606cb794be0a8381470d95c57ce5be03fb3d0ef57c7853b86", size = 278890, upload-time = "2026-02-20T20:19:39.263Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/a3/90/42762b77a5b6aa96cd8c0e80612663d39211e8ae8a6cd47c7f1249a66262/greenlet-3.3.2-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ebd458fa8285960f382841da585e02201b53a5ec2bac6b156fc623b5ce4499f", size = 581120, upload-time = "2026-02-20T20:47:30.161Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/bf/6f/f3d64f4fa0a9c7b5c5b3c810ff1df614540d5aa7d519261b53fba55d4df9/greenlet-3.3.2-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a443358b33c4ec7b05b79a7c8b466f5d275025e750298be7340f8fc63dff2a55", size = 594363, upload-time = "2026-02-20T20:55:56.965Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/9c/8b/1430a04657735a3f23116c2e0d5eb10220928846e4537a938a41b350bed6/greenlet-3.3.2-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4375a58e49522698d3e70cc0b801c19433021b5c37686f7ce9c65b0d5c8677d2", size = 605046, upload-time = "2026-02-20T21:02:45.234Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/72/83/3e06a52aca8128bdd4dcd67e932b809e76a96ab8c232a8b025b2850264c5/greenlet-3.3.2-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e2cd90d413acbf5e77ae41e5d3c9b3ac1d011a756d7284d7f3f2b806bbd6358", size = 594156, upload-time = "2026-02-20T20:20:59.955Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/70/79/0de5e62b873e08fe3cef7dbe84e5c4bc0e8ed0c7ff131bccb8405cd107c8/greenlet-3.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:442b6057453c8cb29b4fb36a2ac689382fc71112273726e2423f7f17dc73bf99", size = 1554649, upload-time = "2026-02-20T20:49:32.293Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/5a/00/32d30dee8389dc36d42170a9c66217757289e2afb0de59a3565260f38373/greenlet-3.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:45abe8eb6339518180d5a7fa47fa01945414d7cca5ecb745346fc6a87d2750be", size = 1619472, upload-time = "2026-02-20T20:21:07.966Z" },
@@ -692,7 +715,6 @@ wheels = [
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/ea/ab/1608e5a7578e62113506740b88066bf09888322a311cff602105e619bd87/greenlet-3.3.2-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:ac8d61d4343b799d1e526db579833d72f23759c71e07181c2d2944e429eb09cd", size = 280358, upload-time = "2026-02-20T20:17:43.971Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/a5/23/0eae412a4ade4e6623ff7626e38998cb9b11e9ff1ebacaa021e4e108ec15/greenlet-3.3.2-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ceec72030dae6ac0c8ed7591b96b70410a8be370b6a477b1dbc072856ad02bd", size = 601217, upload-time = "2026-02-20T20:47:31.462Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/f8/16/5b1678a9c07098ecb9ab2dd159fafaf12e963293e61ee8d10ecb55273e5e/greenlet-3.3.2-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a2a5be83a45ce6188c045bcc44b0ee037d6a518978de9a5d97438548b953a1ac", size = 611792, upload-time = "2026-02-20T20:55:58.423Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/5c/c5/cc09412a29e43406eba18d61c70baa936e299bc27e074e2be3806ed29098/greenlet-3.3.2-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ae9e21c84035c490506c17002f5c8ab25f980205c3e61ddb3a2a2a2e6c411fcb", size = 626250, upload-time = "2026-02-20T21:02:46.596Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/50/1f/5155f55bd71cabd03765a4aac9ac446be129895271f73872c36ebd4b04b6/greenlet-3.3.2-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43e99d1749147ac21dde49b99c9abffcbc1e2d55c67501465ef0930d6e78e070", size = 613875, upload-time = "2026-02-20T20:21:01.102Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/fc/dd/845f249c3fcd69e32df80cdab059b4be8b766ef5830a3d0aa9d6cad55beb/greenlet-3.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4c956a19350e2c37f2c48b336a3afb4bff120b36076d9d7fb68cb44e05d95b79", size = 1571467, upload-time = "2026-02-20T20:49:33.495Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/2a/50/2649fe21fcc2b56659a452868e695634722a6655ba245d9f77f5656010bf/greenlet-3.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6c6f8ba97d17a1e7d664151284cb3315fc5f8353e75221ed4324f84eb162b395", size = 1640001, upload-time = "2026-02-20T20:21:09.154Z" },
@@ -701,7 +723,6 @@ wheels = [
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/ac/48/f8b875fa7dea7dd9b33245e37f065af59df6a25af2f9561efa8d822fde51/greenlet-3.3.2-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:aa6ac98bdfd716a749b84d4034486863fd81c3abde9aa3cf8eff9127981a4ae4", size = 279120, upload-time = "2026-02-20T20:19:01.9Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/49/8d/9771d03e7a8b1ee456511961e1b97a6d77ae1dea4a34a5b98eee706689d3/greenlet-3.3.2-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ab0c7e7901a00bc0a7284907273dc165b32e0d109a6713babd04471327ff7986", size = 603238, upload-time = "2026-02-20T20:47:32.873Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/59/0e/4223c2bbb63cd5c97f28ffb2a8aee71bdfb30b323c35d409450f51b91e3e/greenlet-3.3.2-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d248d8c23c67d2291ffd47af766e2a3aa9fa1c6703155c099feb11f526c63a92", size = 614219, upload-time = "2026-02-20T20:55:59.817Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/94/2b/4d012a69759ac9d77210b8bfb128bc621125f5b20fc398bce3940d036b1c/greenlet-3.3.2-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ccd21bb86944ca9be6d967cf7691e658e43417782bce90b5d2faeda0ff78a7dd", size = 628268, upload-time = "2026-02-20T21:02:48.024Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/7a/34/259b28ea7a2a0c904b11cd36c79b8cef8019b26ee5dbe24e73b469dea347/greenlet-3.3.2-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b6997d360a4e6a4e936c0f9625b1c20416b8a0ea18a8e19cabbefc712e7397ab", size = 616774, upload-time = "2026-02-20T20:21:02.454Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/0a/03/996c2d1689d486a6e199cb0f1cf9e4aa940c500e01bdf201299d7d61fa69/greenlet-3.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:64970c33a50551c7c50491671265d8954046cb6e8e2999aacdd60e439b70418a", size = 1571277, upload-time = "2026-02-20T20:49:34.795Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/d9/c4/2570fc07f34a39f2caf0bf9f24b0a1a0a47bc2e8e465b2c2424821389dfc/greenlet-3.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1a9172f5bf6bd88e6ba5a84e0a68afeac9dc7b6b412b245dd64f52d83c81e55b", size = 1640455, upload-time = "2026-02-20T20:21:10.261Z" },
@@ -710,7 +731,6 @@ wheels = [
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/3f/ae/8bffcbd373b57a5992cd077cbe8858fff39110480a9d50697091faea6f39/greenlet-3.3.2-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:8d1658d7291f9859beed69a776c10822a0a799bc4bfe1bd4272bb60e62507dab", size = 279650, upload-time = "2026-02-20T20:18:00.783Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/d1/c0/45f93f348fa49abf32ac8439938726c480bd96b2a3c6f4d949ec0124b69f/greenlet-3.3.2-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:18cb1b7337bca281915b3c5d5ae19f4e76d35e1df80f4ad3c1a7be91fadf1082", size = 650295, upload-time = "2026-02-20T20:47:34.036Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/b3/de/dd7589b3f2b8372069ab3e4763ea5329940fc7ad9dcd3e272a37516d7c9b/greenlet-3.3.2-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c2e47408e8ce1c6f1ceea0dffcdf6ebb85cc09e55c7af407c99f1112016e45e9", size = 662163, upload-time = "2026-02-20T20:56:01.295Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/cd/ac/85804f74f1ccea31ba518dcc8ee6f14c79f73fe36fa1beba38930806df09/greenlet-3.3.2-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e3cb43ce200f59483eb82949bf1835a99cf43d7571e900d7c8d5c62cdf25d2f9", size = 675371, upload-time = "2026-02-20T21:02:49.664Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/d2/d8/09bfa816572a4d83bccd6750df1926f79158b1c36c5f73786e26dbe4ee38/greenlet-3.3.2-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63d10328839d1973e5ba35e98cccbca71b232b14051fd957b6f8b6e8e80d0506", size = 664160, upload-time = "2026-02-20T20:21:04.015Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/48/cf/56832f0c8255d27f6c35d41b5ec91168d74ec721d85f01a12131eec6b93c/greenlet-3.3.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8e4ab3cfb02993c8cc248ea73d7dae6cec0253e9afa311c9b37e603ca9fad2ce", size = 1619181, upload-time = "2026-02-20T20:49:36.052Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/0a/23/b90b60a4aabb4cec0796e55f25ffbfb579a907c3898cd2905c8918acaa16/greenlet-3.3.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:94ad81f0fd3c0c0681a018a976e5c2bd2ca2d9d94895f23e7bb1af4e8af4e2d5", size = 1687713, upload-time = "2026-02-20T20:21:11.684Z" },
@@ -719,7 +739,6 @@ wheels = [
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/98/6d/8f2ef704e614bcf58ed43cfb8d87afa1c285e98194ab2cfad351bf04f81e/greenlet-3.3.2-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:e26e72bec7ab387ac80caa7496e0f908ff954f31065b0ffc1f8ecb1338b11b54", size = 286617, upload-time = "2026-02-20T20:19:29.856Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/5e/0d/93894161d307c6ea237a43988f27eba0947b360b99ac5239ad3fe09f0b47/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b466dff7a4ffda6ca975979bab80bdadde979e29fc947ac3be4451428d8b0e4", size = 655189, upload-time = "2026-02-20T20:47:35.742Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/f5/2c/d2d506ebd8abcb57386ec4f7ba20f4030cbe56eae541bc6fd6ef399c0b41/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b8bddc5b73c9720bea487b3bffdb1840fe4e3656fba3bd40aa1489e9f37877ff", size = 658225, upload-time = "2026-02-20T20:56:02.527Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/d1/67/8197b7e7e602150938049d8e7f30de1660cfb87e4c8ee349b42b67bdb2e1/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:59b3e2c40f6706b05a9cd299c836c6aa2378cabe25d021acd80f13abf81181cf", size = 666581, upload-time = "2026-02-20T21:02:51.526Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/8e/30/3a09155fbf728673a1dea713572d2d31159f824a37c22da82127056c44e4/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b26b0f4428b871a751968285a1ac9648944cea09807177ac639b030bddebcea4", size = 657907, upload-time = "2026-02-20T20:21:05.259Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/f3/fd/d05a4b7acd0154ed758797f0a43b4c0962a843bedfe980115e842c5b2d08/greenlet-3.3.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1fb39a11ee2e4d94be9a76671482be9398560955c9e568550de0224e41104727", size = 1618857, upload-time = "2026-02-20T20:49:37.309Z" },
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/6f/e1/50ee92a5db521de8f35075b5eff060dd43d39ebd46c2181a2042f7070385/greenlet-3.3.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:20154044d9085151bc309e7689d6f7ba10027f8f5a8c0676ad398b951913d89e", size = 1680010, upload-time = "2026-02-20T20:21:13.427Z" },
@@ -1442,12 +1461,6 @@ wheels = [
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/ef/26/a3f51ea9d10efe2c4f85cac78dcbd898b437d2331cf25d470f804e4498b7/sqlite_vfs-0.1.0-py3-none-any.whl", hash = "sha256:c6e27a35e504bb6360fb0d9083d79572367a903788b3b14aea5f1188130e9314", size = 10768, upload-time = "2025-11-08T08:15:18.588Z" },
]
[[package]]
name = "sqlitedict"
version = "2.1.0"
source = { registry = "https://mirrors.ustc.edu.cn/pypi/web/simple/" }
sdist = { url = "https://mirrors.ustc.edu.cn/pypi/packages/12/9a/7620d1e9dcb02839ed6d4b14064e609cdd7a8ae1e47289aa0456796dd9ca/sqlitedict-2.1.0.tar.gz", hash = "sha256:03d9cfb96d602996f1d4c2db2856f1224b96a9c431bdd16e78032a72940f9e8c", size = 21846, upload-time = "2022-12-03T13:39:13.102Z" }
[[package]]
name = "sqlmodel"
version = "0.0.29"
@@ -1635,6 +1648,47 @@ wheels = [
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" },
]
[[package]]
name = "tzdata"
version = "2026.1"
source = { registry = "https://mirrors.ustc.edu.cn/pypi/web/simple/" }
sdist = { url = "https://mirrors.ustc.edu.cn/pypi/packages/19/f5/cd531b2d15a671a40c0f66cf06bc3570a12cd56eef98960068ebbad1bf5a/tzdata-2026.1.tar.gz", hash = "sha256:67658a1903c75917309e753fdc349ac0efd8c27db7a0cb406a25be4840f87f98", size = 197639, upload-time = "2026-04-03T11:25:22.002Z" }
wheels = [
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/b0/70/d460bd685a170790ec89317e9bd33047988e4bce507b831f5db771e142de/tzdata-2026.1-py2.py3-none-any.whl", hash = "sha256:4b1d2be7ac37ceafd7327b961aa3a54e467efbdb563a23655fbfe0d39cfc42a9", size = 348952, upload-time = "2026-04-03T11:25:20.313Z" },
]
[[package]]
name = "tzlocal"
version = "5.2"
source = { registry = "https://mirrors.ustc.edu.cn/pypi/web/simple/" }
resolution-markers = [
"python_full_version < '3.9'",
]
dependencies = [
{ name = "backports-zoneinfo", marker = "python_full_version < '3.9'" },
{ name = "tzdata", marker = "python_full_version < '3.9' and sys_platform == 'win32'" },
]
sdist = { url = "https://mirrors.ustc.edu.cn/pypi/packages/04/d3/c19d65ae67636fe63953b20c2e4a8ced4497ea232c43ff8d01db16de8dc0/tzlocal-5.2.tar.gz", hash = "sha256:8d399205578f1a9342816409cc1e46a93ebd5755e39ea2d85334bea911bf0e6e", size = 30201, upload-time = "2023-10-22T17:41:38.449Z" }
wheels = [
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/97/3f/c4c51c55ff8487f2e6d0e618dba917e3c3ee2caae6cf0fbb59c9b1876f2e/tzlocal-5.2-py3-none-any.whl", hash = "sha256:49816ef2fe65ea8ac19d19aa7a1ae0551c834303d5014c6d5a62e4cbda8047b8", size = 17859, upload-time = "2023-10-22T17:41:36.511Z" },
]
[[package]]
name = "tzlocal"
version = "5.3.1"
source = { registry = "https://mirrors.ustc.edu.cn/pypi/web/simple/" }
resolution-markers = [
"python_full_version >= '3.10'",
"python_full_version == '3.9.*'",
]
dependencies = [
{ name = "tzdata", marker = "python_full_version >= '3.9' and sys_platform == 'win32'" },
]
sdist = { url = "https://mirrors.ustc.edu.cn/pypi/packages/8b/2e/c14812d3d4d9cd1773c6be938f89e5735a1f11a9f184ac3639b93cef35d5/tzlocal-5.3.1.tar.gz", hash = "sha256:cceffc7edecefea1f595541dbd6e990cb1ea3d19bf01b2809f362a03dd7921fd", size = 30761, upload-time = "2025-03-05T21:17:41.549Z" }
wheels = [
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/c2/14/e2a54fabd4f08cd7af1c07030603c3356b74da07f7cc056e600436edfa17/tzlocal-5.3.1-py3-none-any.whl", hash = "sha256:eb1a66c3ef5847adf7a834f1be0800581b683b5608e74f86ecbcef8ab91bb85d", size = 18026, upload-time = "2025-03-05T21:17:39.857Z" },
]
[[package]]
name = "uvicorn"
version = "0.33.0"