modify: diskcache 缓存替换
This commit is contained in:
+9
-4
@@ -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
Binary file not shown.
+8
-1
@@ -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
@@ -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
@@ -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
@@ -4,6 +4,6 @@
|
||||
"pythonVersion": "3.11",
|
||||
"extraPaths": [
|
||||
"./",
|
||||
"./.venv/lib/python3.12/site-packages"
|
||||
"./.venv/lib/python3.11/site-packages"
|
||||
]
|
||||
}
|
||||
+12
-15
@@ -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
|
||||
Binary file not shown.
@@ -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}")
|
||||
|
||||
@@ -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 "请查看服务器日志",
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
@@ -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:
|
||||
"""
|
||||
获取当前用户依赖项
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,4 +1,5 @@
|
||||
from typing import Optional
|
||||
|
||||
from sqlmodel import SQLModel, Field
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()}")
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user