From 211fddd6e06323ef2ba39a359b16c379fee94a7e Mon Sep 17 00:00:00 2001 From: zhangtao <9480807882@qq.com> Date: Sun, 21 Jun 2026 06:02:05 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E5=AE=8C=E6=88=90=E9=A1=B9?= =?UTF-8?q?=E7=9B=AE=E5=A4=A7=E8=A7=84=E6=A8=A1=E4=BB=A3=E7=A0=81=E9=87=8D?= =?UTF-8?q?=E6=9E=84=E4=B8=8E=E4=BE=9D=E8=B5=96=E6=B8=85=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 这是一次综合性的重构更新,包含以下主要变更: 1. 升级Python版本到3.12,更新依赖配置 2. 替换旧的.j2模板为.jinja2格式,新增代码生成模板 3. 重构权限过滤策略,更新权限枚举与模型配置 4. 移除Prefect依赖,替换为自研拓扑并行执行引擎 5. 重构认证与上下文管理,拆分租户/请求上下文 6. 简化响应模型、CRUD与服务层代码 7. 清理废弃的支付网关模块,重构订单定时任务 8. 更新在线用户、监控等模块的接口与路由 9. 优化邮件模板与工具类,新增邮件模板文件 10. 修复数据库会话配置与类型提示 --- backend/.python-version | 2 +- .../api/v1/module_common/file/controller.py | 27 +- .../app/api/v1/module_common/file/service.py | 57 +- .../api/v1/module_common/monitoring/health.py | 2 +- .../api/v1/module_monitor/cache/controller.py | 84 +- .../api/v1/module_monitor/cache/service.py | 108 +- .../v1/module_monitor/online/controller.py | 40 +- .../api/v1/module_monitor/online/schema.py | 2 +- .../api/v1/module_monitor/online/service.py | 93 +- .../v1/module_monitor/resource/controller.py | 112 +- .../api/v1/module_monitor/resource/service.py | 725 ++---- .../v1/module_monitor/server/controller.py | 11 +- .../api/v1/module_monitor/server/service.py | 89 +- .../v1/module_platform/email/controller.py | 166 +- .../api/v1/module_platform/email/service.py | 309 +-- .../v1/module_platform/invoice/controller.py | 88 +- .../v1/module_platform/invoice/pdf_helper.py | 2 +- .../api/v1/module_platform/invoice/service.py | 11 + .../api/v1/module_platform/menu/controller.py | 71 +- .../app/api/v1/module_platform/menu/model.py | 2 +- .../api/v1/module_platform/menu/service.py | 161 +- .../v1/module_platform/order/controller.py | 189 +- .../app/api/v1/module_platform/order/crud.py | 15 - .../api/v1/module_platform/order/service.py | 34 +- .../v1/module_platform/package/controller.py | 127 +- .../api/v1/module_platform/package/service.py | 247 +- .../v1/module_platform/plugin/controller.py | 144 +- .../api/v1/module_platform/plugin/service.py | 220 +- .../self_service/controller.py | 66 +- .../module_platform/self_service/service.py | 7 +- .../v1/module_platform/tenant/controller.py | 199 +- .../api/v1/module_platform/tenant/schema.py | 9 - .../api/v1/module_platform/tenant/service.py | 295 ++- .../api/v1/module_system/auth/controller.py | 171 +- .../v1/module_system/auth/oauth_service.py | 4 +- .../app/api/v1/module_system/auth/schema.py | 8 +- .../app/api/v1/module_system/auth/service.py | 450 +--- .../api/v1/module_system/dept/controller.py | 101 +- .../app/api/v1/module_system/dept/model.py | 2 +- .../app/api/v1/module_system/dept/service.py | 135 +- .../api/v1/module_system/dict/controller.py | 274 +-- .../app/api/v1/module_system/dict/service.py | 288 ++- .../api/v1/module_system/log/controller.py | 110 +- .../app/api/v1/module_system/log/service.py | 145 +- .../api/v1/module_system/notice/controller.py | 125 +- .../api/v1/module_system/notice/service.py | 244 +- .../api/v1/module_system/params/controller.py | 140 +- .../api/v1/module_system/params/service.py | 123 +- .../v1/module_system/position/controller.py | 98 +- .../api/v1/module_system/position/service.py | 144 +- .../api/v1/module_system/role/controller.py | 111 +- .../app/api/v1/module_system/role/model.py | 2 +- .../app/api/v1/module_system/role/service.py | 85 +- .../api/v1/module_system/ticket/controller.py | 101 +- .../api/v1/module_system/ticket/service.py | 134 +- .../api/v1/module_system/user/controller.py | 207 +- .../app/api/v1/module_system/user/service.py | 467 +--- backend/app/common/enums.py | 14 +- backend/app/common/response.py | 5 +- backend/app/config/setting.py | 59 +- backend/app/core/base_crud.py | 165 +- backend/app/core/base_schema.py | 29 +- backend/app/core/database.py | 4 +- backend/app/core/dependencies.py | 71 +- backend/app/core/exceptions.py | 260 +-- backend/app/core/logger.py | 107 +- backend/app/core/middlewares.py | 329 +-- backend/app/core/payment/__init__.py | 58 - backend/app/core/payment/alipay.py | 120 - backend/app/core/payment/base.py | 59 - backend/app/core/payment/mock.py | 68 - backend/app/core/permission.py | 32 +- backend/app/core/request_context.py | 47 + backend/app/core/router_class.py | 100 +- backend/app/core/tenant.py | 54 - backend/app/core/tenant_filter.py | 226 -- backend/app/init_app.py | 156 +- .../app/plugin/module_ai/chat/controller.py | 84 +- backend/app/plugin/module_ai/chat/service.py | 176 +- .../plugin/module_example/demo/controller.py | 122 +- .../app/plugin/module_example/demo/service.py | 175 +- .../module_generator/gencode/controller.py | 179 +- .../module_generator/gencode/service.py | 236 +- .../module_task/cronjob/job/controller.py | 142 +- .../plugin/module_task/cronjob/job/service.py | 156 +- .../module_task/cronjob/node/controller.py | 122 +- .../module_task/cronjob/node/service.py | 184 +- backend/app/plugin/module_task/plugin.toml | 2 +- .../plugin/module_task/workflow/__init__.py | 2 +- .../module_task/workflow/flows/controller.py | 89 +- .../module_task/workflow/flows/model.py | 2 +- .../module_task/workflow/flows/service.py | 183 +- .../module_task/workflow/handlers/__init__.py | 6 +- .../workflow/handlers/prefect_engine.py | 232 -- .../workflow/handlers/workflow_engine.py | 136 ++ .../module_task/workflow/nodes/controller.py | 92 +- .../module_task/workflow/nodes/model.py | 2 +- .../module_task/workflow/nodes/service.py | 162 +- backend/app/scripts/initialize.py | 14 +- backend/app/utils/console.py | 4 +- backend/app/utils/email_util.py | 35 +- backend/app/utils/jinja2_template_util.py | 38 +- backend/app/utils/payment.py | 193 ++ backend/main.py | 11 +- backend/pyproject.toml | 14 +- backend/requirements.txt | 16 +- .../templates/emails/reset_password.jinja2 | 31 + backend/templates/emails/welcome.jinja2 | 27 + .../{invoice.html.j2 => invoice.jinja2} | 0 .../{__init__.py.j2 => __init__.py.jinja2} | 0 ...{controller.py.j2 => controller.py.jinja2} | 0 .../python/{crud.py.j2 => crud.py.jinja2} | 0 .../python/{model.py.j2 => model.py.jinja2} | 0 .../python/{schema.py.j2 => schema.py.jinja2} | 0 .../{service.py.j2 => service.py.jinja2} | 0 .../templates/ts/{api.ts.j2 => api.ts.jinja2} | 0 .../vue/{index.vue.j2 => index.vue.jinja2} | 0 backend/tests/test_system_modules.py | 6 +- backend/tests/test_unit.py | 13 +- backend/uv.lock | 2014 ++--------------- frontend/web/src/api/module_monitor/online.ts | 2 +- 121 files changed, 2869 insertions(+), 11379 deletions(-) delete mode 100644 backend/app/core/payment/__init__.py delete mode 100644 backend/app/core/payment/alipay.py delete mode 100644 backend/app/core/payment/base.py delete mode 100644 backend/app/core/payment/mock.py create mode 100644 backend/app/core/request_context.py delete mode 100644 backend/app/core/tenant.py delete mode 100644 backend/app/core/tenant_filter.py delete mode 100644 backend/app/plugin/module_task/workflow/handlers/prefect_engine.py create mode 100644 backend/app/plugin/module_task/workflow/handlers/workflow_engine.py create mode 100644 backend/app/utils/payment.py create mode 100644 backend/templates/emails/reset_password.jinja2 create mode 100644 backend/templates/emails/welcome.jinja2 rename backend/templates/{invoice.html.j2 => invoice.jinja2} (100%) rename backend/templates/python/{__init__.py.j2 => __init__.py.jinja2} (100%) rename backend/templates/python/{controller.py.j2 => controller.py.jinja2} (100%) rename backend/templates/python/{crud.py.j2 => crud.py.jinja2} (100%) rename backend/templates/python/{model.py.j2 => model.py.jinja2} (100%) rename backend/templates/python/{schema.py.j2 => schema.py.jinja2} (100%) rename backend/templates/python/{service.py.j2 => service.py.jinja2} (100%) rename backend/templates/ts/{api.ts.j2 => api.ts.jinja2} (100%) rename backend/templates/vue/{index.vue.j2 => index.vue.jinja2} (100%) diff --git a/backend/.python-version b/backend/.python-version index c8cfe395..e4fba218 100644 --- a/backend/.python-version +++ b/backend/.python-version @@ -1 +1 @@ -3.10 +3.12 diff --git a/backend/app/api/v1/module_common/file/controller.py b/backend/app/api/v1/module_common/file/controller.py index 0603337e..262b74ed 100644 --- a/backend/app/api/v1/module_common/file/controller.py +++ b/backend/app/api/v1/module_common/file/controller.py @@ -21,8 +21,7 @@ from app.utils.upload_util import UploadUtil from .service import FileService -FileRouter = APIRouter(route_class=OperationLogRoute, prefix="/file", tags=["文件管理"]) - +FileRouter = APIRouter(route_class=OperationLogRoute, prefix="/file", tags=["公共模块", "文件管理"]) @FileRouter.post( "/upload", @@ -39,18 +38,6 @@ async def upload_controller( ] = "file", target_path: Annotated[str | None, Form(description="目标目录路径(仅 resource 类型支持)")] = None, ) -> JSONResponse: - """ - 统一文件上传接口 - - 参数: - - file (UploadFile): 上传的文件 - - request (Request): 请求对象 - - upload_type (str): 上传类型,默认 "file" - - target_path (str | None): 目标目录路径,仅 resource 类型支持 - - 返回: - - JSONResponse: 包含上传文件详情的JSON响应 - """ result = await FileService.upload_service( base_url=str(request.base_url), file=file, @@ -59,7 +46,6 @@ async def upload_controller( ) return SuccessResponse(data=result, msg="上传文件成功") - @FileRouter.post( "/download", summary="下载文件", @@ -70,17 +56,6 @@ async def download_controller( file_path: Annotated[str, Body(description="文件路径")], delete: Annotated[bool, Body(description="是否删除文件")] = False, ) -> FileResponse: - """ - 下载文件 - - 参数: - - background_tasks (BackgroundTasks): 后台任务对象 - - file_path (str): 文件路径 - - delete (bool): 是否删除文件 - - 返回: - - FileResponse: 包含下载文件的响应 - """ result = await FileService.download_service(file_path=file_path) if delete: background_tasks.add_task(UploadUtil.delete_file, Path(result.file_path)) diff --git a/backend/app/api/v1/module_common/file/service.py b/backend/app/api/v1/module_common/file/service.py index 92a11946..e4ac2dbc 100644 --- a/backend/app/api/v1/module_common/file/service.py +++ b/backend/app/api/v1/module_common/file/service.py @@ -22,21 +22,8 @@ class FileService: upload_type: str = "file", target_path: str | None = None, ) -> UploadResponseSchema: - """ - 上传文件。 - - 参数: - - base_url (str): 基础访问 URL。 - - file (UploadFile): 上传文件对象。 - - upload_type (str): 上传类型,'file'、'avatar'、'param'、'resource',默认 'file'。 - - target_path (str | None): 目标目录路径,仅 resource 类型支持。 - - 返回: - - Dict: 上传响应字典。 - - 异常: - - CustomException: 当未选择文件或上传类型错误时抛出。 - """ + """上传文件""" + filename, filepath, file_url = await UploadUtil.upload_file( file=file, base_url=base_url, @@ -51,20 +38,10 @@ class FileService: file_url=f"{file_url}", ) - @staticmethod - def _validate_download_path(file_path: str) -> str: - """ - 验证下载路径是否安全。 + @classmethod + async def download_service(cls, file_path: str) -> DownloadFileSchema: + """下载文件""" - 参数: - - file_path (str): 文件路径。 - - 返回: - - str: 安全的绝对路径。 - - 异常: - - CustomException: 当路径不安全时抛出。 - """ if not file_path: raise CustomException(msg="请选择要下载的文件") @@ -81,30 +58,12 @@ class FileService: logger.error(f"路径不在上传目录内: {file_path}") raise CustomException(msg="非法的文件路径") - return abs_path - - @classmethod - async def download_service(cls, file_path: str) -> DownloadFileSchema: - """ - 下载文件。 - - 参数: - - file_path (str): 文件路径。 - - 返回: - - DownloadFileSchema: 下载文件响应对象。 - - 异常: - - CustomException: 当未选择文件或文件不存在时抛出。 - """ - safe_path = cls._validate_download_path(file_path) - - if not UploadUtil.check_file_exists(safe_path): + if not UploadUtil.check_file_exists(abs_path): raise CustomException(msg="文件不存在") - file_name = UploadUtil.download_file(safe_path) + file_name = UploadUtil.download_file(abs_path) return DownloadFileSchema( - file_path=safe_path, + file_path=abs_path, file_name=str(file_name), ) diff --git a/backend/app/api/v1/module_common/monitoring/health.py b/backend/app/api/v1/module_common/monitoring/health.py index ef17f10c..26f4e37c 100644 --- a/backend/app/api/v1/module_common/monitoring/health.py +++ b/backend/app/api/v1/module_common/monitoring/health.py @@ -22,7 +22,7 @@ from app.core.logger import logger from .schema import DependencyStatus, HealthOut, ReadinessOut -HealthRouter = APIRouter(prefix="/health", tags=["健康检查"]) +HealthRouter = APIRouter(prefix="/health", tags=["公共模块", "健康检查"]) async def _check_database() -> DependencyStatus: diff --git a/backend/app/api/v1/module_monitor/cache/controller.py b/backend/app/api/v1/module_monitor/cache/controller.py index 7b398c83..242912b3 100644 --- a/backend/app/api/v1/module_monitor/cache/controller.py +++ b/backend/app/api/v1/module_monitor/cache/controller.py @@ -7,12 +7,11 @@ from redis.asyncio.client import Redis from app.api.v1.module_monitor.cache.schema import CacheInfoSchema, CacheMonitorSchema from app.common.response import ResponseSchema, SuccessResponse from app.core.dependencies import AuthPermission, redis_getter -from app.core.exceptions import CustomException from app.core.router_class import OperationLogRoute from .service import CacheService -CacheRouter = APIRouter(route_class=OperationLogRoute, prefix="/cache", tags=["缓存监控"]) +CacheRouter = APIRouter(route_class=OperationLogRoute, prefix="/cache", tags=["系统监控", "缓存监控"]) @CacheRouter.get( @@ -24,16 +23,7 @@ CacheRouter = APIRouter(route_class=OperationLogRoute, prefix="/cache", tags=[" async def get_monitor_cache_info_controller( redis: Annotated[Redis, Depends(redis_getter)], ) -> JSONResponse: - """ - 获取缓存监控统计信息 - - 参数: - - redis (Redis): Redis 客户端对象 - - 返回: - - JSONResponse: 包含缓存监控统计信息的JSON响应 - """ - result = await CacheService.get_cache_monitor_statistical_info_service(redis=redis) + result = await CacheService.get_monitor_statistical_info(redis=redis) return SuccessResponse(data=result, msg="获取缓存监控信息成功") @@ -44,13 +34,7 @@ async def get_monitor_cache_info_controller( response_model=ResponseSchema[list[CacheInfoSchema]], ) async def get_monitor_cache_name_controller() -> JSONResponse: - """ - 获取缓存名称列表 - - 返回: - - JSONResponse: 包含缓存名称列表的JSON响应 - """ - result = await CacheService.get_cache_monitor_cache_name_service() + result = await CacheService.get_monitor_cache_names() return SuccessResponse(data=result, msg="获取缓存名称列表成功") @@ -61,16 +45,7 @@ async def get_monitor_cache_name_controller() -> JSONResponse: response_model=ResponseSchema[list[CacheInfoSchema]], ) async def get_monitor_cache_key_controller(cache_name: str, redis: Annotated[Redis, Depends(redis_getter)]) -> JSONResponse: - """ - 获取指定缓存名称下的键名列表 - - 参数: - - cache_name (str): 缓存名称 - - 返回: - - JSONResponse: 包含缓存键名列表的JSON响应 - """ - result = await CacheService.get_cache_monitor_cache_key_service(redis=redis, cache_name=cache_name) + result = await CacheService.get_monitor_cache_keys(redis=redis, cache_name=cache_name) return SuccessResponse(data=result, msg=f"获取缓存{cache_name}的键名列表成功") @@ -85,17 +60,7 @@ async def get_monitor_cache_value_controller( cache_key: str, redis: Annotated[Redis, Depends(redis_getter)], ) -> JSONResponse: - """ - 获取指定缓存键的值 - - 参数: - - cache_name (str): 缓存名称 - - cache_key (str): 缓存键 - - 返回: - - JSONResponse: 包含缓存值的JSON响应 - """ - result = await CacheService.get_cache_monitor_cache_value_service(redis=redis, cache_name=cache_name, cache_key=cache_key) + result = await CacheService.get_monitor_cache_value(redis=redis, cache_name=cache_name, cache_key=cache_key) return SuccessResponse(data=result, msg=f"获取缓存{cache_name}:{cache_key}的值成功") @@ -106,18 +71,7 @@ async def get_monitor_cache_value_controller( response_model=ResponseSchema[None], ) async def clear_monitor_cache_name_controller(cache_name: str, redis: Annotated[Redis, Depends(redis_getter)]) -> JSONResponse: - """ - 清除指定缓存名称下的所有缓存 - - 参数: - - cache_name (str): 缓存名称 - - 返回: - - JSONResponse: 包含清除结果的JSON响应 - """ - result = await CacheService.clear_cache_monitor_cache_name_service(redis=redis, cache_name=cache_name) - if not result: - raise CustomException(msg="清除缓存失败", data=result) + result = await CacheService.clear_monitor_cache_by_name(redis=redis, cache_name=cache_name) return SuccessResponse(msg=f"{cache_name}对应键值清除成功", data=result) @@ -128,18 +82,7 @@ async def clear_monitor_cache_name_controller(cache_name: str, redis: Annotated[ response_model=ResponseSchema[None], ) async def clear_monitor_cache_key_controller(cache_key: str, redis: Annotated[Redis, Depends(redis_getter)]) -> JSONResponse: - """ - 清除指定缓存键 - - 参数: - - cache_key (str): 缓存键 - - 返回: - - JSONResponse: 包含清除结果的JSON响应 - """ - result = await CacheService.clear_cache_monitor_cache_key_service(redis=redis, cache_key=cache_key) - if not result: - raise CustomException(msg="清除缓存失败", data=result) + result = await CacheService.clear_monitor_cache_by_key(redis=redis, cache_key=cache_key) return SuccessResponse(msg=f"{cache_key}清除成功", data=result) @@ -152,16 +95,5 @@ async def clear_monitor_cache_key_controller(cache_key: str, redis: Annotated[Re async def clear_monitor_cache_all_controller( redis: Annotated[Redis, Depends(redis_getter)], ) -> JSONResponse: - """ - 清除所有缓存 - - 参数: - - redis (Redis): Redis 客户端对象 - - 返回: - - JSONResponse: 包含清除结果的JSON响应 - """ - result = await CacheService.clear_cache_monitor_all_service(redis=redis) - if not result: - raise CustomException(msg="清除缓存失败", data=result) + result = await CacheService.clear_monitor_cache_all(redis=redis) return SuccessResponse(msg="所有缓存清除成功", data=result) diff --git a/backend/app/api/v1/module_monitor/cache/service.py b/backend/app/api/v1/module_monitor/cache/service.py index 1cb6c55e..38abefaa 100644 --- a/backend/app/api/v1/module_monitor/cache/service.py +++ b/backend/app/api/v1/module_monitor/cache/service.py @@ -7,21 +7,10 @@ from .schema import CacheInfoSchema, CacheMonitorSchema class CacheService: - """ - 缓存监控模块服务层 - """ + """缓存监控模块服务层""" - @classmethod - async def get_cache_monitor_statistical_info_service(cls, redis: Redis) -> CacheMonitorSchema: - """ - 获取缓存监控信息。 - - 参数: - - redis (Redis): Redis 对象。 - - 返回: - - dict: 缓存监控信息字典。 - """ + @staticmethod + async def get_monitor_statistical_info(redis: Redis) -> CacheMonitorSchema: info = await RedisCURD(redis).info() db_size = await RedisCURD(redis).db_size() command_stats_dict = await RedisCURD(redis).commandstats() @@ -29,14 +18,8 @@ class CacheService: command_stats = [{"name": key.split("_")[1], "value": str(value.get("calls"))} for key, value in command_stats_dict.items()] return CacheMonitorSchema(command_stats=command_stats, db_size=db_size, info=info) - @classmethod - async def get_cache_monitor_cache_name_service(cls) -> list[CacheInfoSchema]: - """ - 获取缓存名称列表信息。 - - 返回: - - list: 缓存名称列表信息。 - """ + @staticmethod + async def get_monitor_cache_names() -> list[CacheInfoSchema]: return [ CacheInfoSchema( cache_key="", @@ -47,38 +30,14 @@ class CacheService: for key_config in RedisInitKeyConfig ] - @classmethod - async def get_cache_monitor_cache_key_service(cls, redis: Redis, cache_name: str) -> list: - """ - 获取缓存键名列表信息。 - - 参数: - - redis (Redis): Redis 对象。 - - cache_name (str): 缓存名称。 - - 返回: - - list: 缓存键名列表信息。 - """ + @staticmethod + async def get_monitor_cache_keys(redis: Redis, cache_name: str) -> list: cache_keys = await RedisCURD(redis).get_keys(f"{cache_name}*") - cache_key_list = [key.split(":", 1)[1] for key in cache_keys if key.startswith(f"{cache_name}:")] + return [key.split(":", 1)[1] for key in cache_keys if key.startswith(f"{cache_name}:")] - return cache_key_list - - @classmethod - async def get_cache_monitor_cache_value_service(cls, redis: Redis, cache_name: str, cache_key: str) -> CacheInfoSchema: - """ - 获取缓存内容信息。 - - 参数: - - redis (Redis): Redis 对象。 - - cache_name (str): 缓存名称。 - - cache_key (str): 缓存键名。 - - 返回: - - dict: 缓存内容信息字典。 - """ + @staticmethod + async def get_monitor_cache_value(redis: Redis, cache_name: str, cache_key: str) -> CacheInfoSchema: cache_value = await RedisCURD(redis).get(f"{cache_name}:{cache_key}") - return CacheInfoSchema( cache_key=cache_key, cache_name=cache_name, @@ -86,58 +45,23 @@ class CacheService: remark="", ) - @classmethod - async def clear_cache_monitor_cache_name_service(cls, redis: Redis, cache_name: str) -> bool: - """ - 清除指定缓存名称对应的所有键值。 - - 参数: - - redis (Redis): Redis 对象。 - - cache_name (str): 缓存名称。 - - 返回: - - bool: 是否清理成功。 - """ + @staticmethod + async def clear_monitor_cache_by_name(redis: Redis, cache_name: str) -> bool: cache_keys = await RedisCURD(redis).get_keys(f"{cache_name}*") if cache_keys: await RedisCURD(redis).delete(*cache_keys) - return True - @classmethod - async def clear_cache_monitor_cache_key_service(cls, redis: Redis, cache_key: str) -> bool: - """ - 清除匹配指定键名的所有键值。 - - 参数: - - redis (Redis): Redis 对象。 - - cache_key (str): 缓存键名。 - - 返回: - - bool: 是否清理成功。 - """ + @staticmethod + async def clear_monitor_cache_by_key(redis: Redis, cache_key: str) -> bool: cache_keys = await RedisCURD(redis).get_keys(f"*{cache_key}") if cache_keys: await RedisCURD(redis).delete(*cache_keys) - return True - @classmethod - async def clear_cache_monitor_all_service(cls, redis: Redis) -> bool: - """ - 清除所有缓存。 - - 参数: - - redis (Redis): Redis 对象。 - - 返回: - - bool: 是否清理成功。 - """ + @staticmethod + async def clear_monitor_cache_all(redis: Redis) -> bool: cache_keys = await RedisCURD(redis).get_keys() if cache_keys: await RedisCURD(redis).delete(*cache_keys) - return True - - # 避免清除所有的缓存,而采用上面的方式,只清除本系统内指定的所有缓存 - # return await RedisCURD(redis).clear() diff --git a/backend/app/api/v1/module_monitor/online/controller.py b/backend/app/api/v1/module_monitor/online/controller.py index feb3c8b3..b77ee48f 100644 --- a/backend/app/api/v1/module_monitor/online/controller.py +++ b/backend/app/api/v1/module_monitor/online/controller.py @@ -13,7 +13,7 @@ from app.core.router_class import OperationLogRoute from .schema import OnlineOutSchema, OnlineQueryParam from .service import OnlineService -OnlineRouter = APIRouter(route_class=OperationLogRoute, prefix="/online", tags=["在线用户"]) +OnlineRouter = APIRouter(route_class=OperationLogRoute, prefix="/online", tags=["系统监控", "在线用户"]) @OnlineRouter.get( @@ -27,25 +27,12 @@ async def get_online_list_controller( paging_query: Annotated[PaginationQueryParam, Depends()], search: Annotated[OnlineQueryParam, Depends()], ) -> JSONResponse: - """ - 获取在线用户列表 - - 参数: - - redis (Redis): Redis异步客户端实例。 - - paging_query (PaginationQueryParam): 分页查询参数模型。 - - search (OnlineQueryParam): 查询参数模型。 - - 返回: - - JSONResponse: 包含在线用户列表的JSON响应。 - """ - result_dict_list = await OnlineService.get_online_list_service(redis=redis, search=search) - # 在线用户来自 Redis,无法在数据库层 OFFSET/LIMIT + result_dict_list = await OnlineService.get_online_list(redis=redis, search=search) result_dict = await PaginationService.paginate( data_list=result_dict_list, page_no=paging_query.page_no, page_size=paging_query.page_size, ) - return SuccessResponse(data=result_dict, msg="获取成功") @@ -59,17 +46,7 @@ async def delete_online_controller( session_id: Annotated[str, Body(description="会话编号")], redis: Annotated[Redis, Depends(redis_getter)], ) -> JSONResponse: - """ - 强制下线指定在线用户 - - 参数: - - session_id (str): 在线用户会话ID。 - - redis (Redis): Redis异步客户端实例。 - - 返回: - - JSONResponse: 包含操作结果的JSON响应。 - """ - await OnlineService.delete_online_service(redis=redis, session_id=session_id) + await OnlineService.delete_online(redis=redis, session_id=session_id) return SuccessResponse(msg="强制下线成功") @@ -82,14 +59,5 @@ async def delete_online_controller( async def clear_online_controller( redis: Annotated[Redis, Depends(redis_getter)], ) -> JSONResponse: - """ - 清除所有在线用户 - - 参数: - - redis (Redis): Redis异步客户端实例。 - - 返回: - - JSONResponse: 包含操作结果的JSON响应。 - """ - await OnlineService.clear_online_service(redis=redis) + await OnlineService.clear_online(redis=redis) return SuccessResponse(msg="清除所有在线用户成功") diff --git a/backend/app/api/v1/module_monitor/online/schema.py b/backend/app/api/v1/module_monitor/online/schema.py index 819772ae..b031ba12 100644 --- a/backend/app/api/v1/module_monitor/online/schema.py +++ b/backend/app/api/v1/module_monitor/online/schema.py @@ -16,7 +16,7 @@ class OnlineOutSchema(BaseModel): session_id: str = Field(..., description="会话编号") user_id: int = Field(..., description="用户ID") tenant_id: int = Field(..., description="租户ID") - is_super_admin: bool = Field(default=False, description="是否为超级管理员") + is_superuser: bool = Field(default=False, description="是否为超级管理员") user_name: str = Field(..., description="用户名") ipaddr: str | None = Field(default=None, description="登陆IP地址") login_location: str | None = Field(default=None, description="登录所属地") diff --git a/backend/app/api/v1/module_monitor/online/service.py b/backend/app/api/v1/module_monitor/online/service.py index 795e23f3..046e3fcc 100644 --- a/backend/app/api/v1/module_monitor/online/service.py +++ b/backend/app/api/v1/module_monitor/online/service.py @@ -13,19 +13,8 @@ from .schema import OnlineQueryParam class OnlineService: """在线用户管理模块服务层""" - @classmethod - async def get_online_list_service(cls, redis: Redis, search: OnlineQueryParam | None = None) -> list[dict]: - """ - 获取在线用户列表信息(支持分页和搜索) - - 参数: - - redis (Redis): Redis异步客户端实例。 - - search (OnlineQueryParam | None): 查询参数模型。 - - 返回: - - list[dict]: 在线用户详情字典列表。 - """ - + @staticmethod + async def get_online_list(redis: Redis, search: OnlineQueryParam | None = None) -> list[dict]: keys = await RedisCURD(redis).get_keys(f"{RedisInitKeyConfig.ACCESS_TOKEN.key}:*") tokens = await RedisCURD(redis).mget(keys) @@ -36,73 +25,39 @@ class OnlineService: try: payload = decode_access_token(token=token) session_info = json.loads(payload.sub) - if cls._match_search_conditions(session_info, search): - online_users.append(session_info) + + # 内联搜索匹配逻辑 + if search: + if search.name and search.name[1]: + kw = search.name[1].strip("%") + if kw.lower() not in session_info.get("name", "").lower(): + continue + if search.ipaddr and search.ipaddr[1]: + kw = search.ipaddr[1].strip("%") + if kw not in session_info.get("ipaddr", ""): + continue + if search.login_location and search.login_location[1]: + kw = search.login_location[1].strip("%") + if kw.lower() not in session_info.get("login_location", "").lower(): + continue + + online_users.append(session_info) except Exception as e: logger.error(f"解析在线用户数据失败: {e}") continue - # 按照 login_time 倒序排序 - online_users.sort(key=lambda x: x.get("login_time", ""), reverse=True) + online_users.sort(key=lambda x: x.get("login_time", ""), reverse=True) return online_users - @classmethod - async def delete_online_service(cls, redis: Redis, session_id: str) -> None: - """ - 强制下线指定在线用户 - - 参数: - - redis (Redis): Redis异步客户端实例。 - - session_id (str): 在线用户会话ID。 - """ - # 删除 token + @staticmethod + async def delete_online(redis: Redis, session_id: str) -> None: await RedisCURD(redis).delete(f"{RedisInitKeyConfig.ACCESS_TOKEN.key}:{session_id}") await RedisCURD(redis).delete(f"{RedisInitKeyConfig.REFRESH_TOKEN.key}:{session_id}") - logger.info(f"强制下线用户会话: {session_id}") - @classmethod - async def clear_online_service(cls, redis: Redis) -> None: - """ - 强制下线所有在线用户 - - 参数: - - redis (Redis): Redis异步客户端实例。 - """ - # 删除 token + @staticmethod + async def clear_online(redis: Redis) -> None: await RedisCURD(redis).clear(f"{RedisInitKeyConfig.ACCESS_TOKEN.key}:*") await RedisCURD(redis).clear(f"{RedisInitKeyConfig.REFRESH_TOKEN.key}:*") - logger.info("清除所有在线用户会话成功") - @staticmethod - def _match_search_conditions(online_info: dict, search: OnlineQueryParam | None = None) -> bool: - """ - 检查是否匹配搜索条件 - - 参数: - - online_info (dict): 在线用户信息字典。 - - search (OnlineQueryParam | None): 查询参数模型。 - - 返回: - - bool: 如果匹配则返回True,否则返回False。 - """ - if not search: - return True - - if search.name and search.name[1]: - keyword = search.name[1].strip("%") - if keyword.lower() not in online_info.get("name", "").lower(): - return False - - if search.ipaddr and search.ipaddr[1]: - keyword = search.ipaddr[1].strip("%") - if keyword not in online_info.get("ipaddr", ""): - return False - - if search.login_location and search.login_location[1]: - keyword = search.login_location[1].strip("%") - if keyword.lower() not in online_info.get("login_location", "").lower(): - return False - - return True diff --git a/backend/app/api/v1/module_monitor/resource/controller.py b/backend/app/api/v1/module_monitor/resource/controller.py index a12a772e..953bc15f 100644 --- a/backend/app/api/v1/module_monitor/resource/controller.py +++ b/backend/app/api/v1/module_monitor/resource/controller.py @@ -22,7 +22,7 @@ from .schema import ( ) from .service import ResourceService -ResourceRouter = APIRouter(route_class=OperationLogRoute, prefix="/resource", tags=["资源管理"]) +ResourceRouter = APIRouter(route_class=OperationLogRoute, prefix="/resource", tags=["系统监控", "资源管理"]) @ResourceRouter.get( @@ -36,26 +36,12 @@ async def get_directory_list_controller( page: Annotated[PaginationQueryParam, Depends()], search: Annotated[ResourceSearchQueryParam, Depends()], ) -> JSONResponse: - """ - 获取目录列表 - - 参数: - - request (Request): FastAPI请求对象,用于获取基础URL。 - - page (PaginationQueryParam): 分页查询参数模型。 - - search (ResourceSearchQueryParam): 资源查询参数模型。 - - 返回: - - JSONResponse: 包含目录列表的JSON响应。 - """ - # 获取资源列表(与案例模块保持一致的分页实现) - result_dict_list = await ResourceService.get_resources_list_service(search=search, base_url=str(request.base_url)) - # 目录列表来自本地文件系统扫描,无 SQL 分页 + result_dict_list = await ResourceService.get_resources_list(search=search, base_url=str(request.base_url)) result_dict = await PaginationService.paginate( data_list=result_dict_list, page_no=page.page_no, page_size=page.page_size, ) - return SuccessResponse(data=result_dict, msg="获取目录列表成功") @@ -70,18 +56,6 @@ async def upload_file_controller( request: Request, target_path: Annotated[str | None, Form(description="目标目录路径")] = None, ) -> JSONResponse: - """ - 上传文件 - - 参数: - - file (UploadFile): 要上传的文件对象。 - - request (Request): FastAPI请求对象,用于获取基础URL。 - - target_path (str | None): 目标目录路径,默认为None。 - - 返回: - - JSONResponse: 包含上传文件信息的JSON响应。 - """ - # 调用统一上传接口,使用 resource 类型 result = await FileService.upload_service( base_url=str(request.base_url), file=file, @@ -99,18 +73,8 @@ async def upload_file_controller( async def download_file_controller( path: Annotated[str, Query(description="文件路径")], ) -> FileResponse: - """ - 下载文件 + file_path = await ResourceService.download_file(file_path=path) - 参数: - - path (str): 文件路径。 - - 返回: - - FileResponse: 包含文件内容的文件响应。 - """ - file_path = await ResourceService.download_file_service(file_path=path) - - # 获取文件名 import os filename = os.path.basename(file_path) @@ -131,16 +95,7 @@ async def download_file_controller( async def delete_files_controller( paths: Annotated[list[str], Body(description="文件路径列表")], ) -> JSONResponse: - """ - 删除文件 - - 参数: - - paths (list[str]): 文件路径列表。 - - 返回: - - JSONResponse: 包含删除结果的JSON响应。 - """ - await ResourceService.delete_file_service(paths=paths) + await ResourceService.delete_file(paths=paths) return SuccessResponse(msg="删除文件成功") @@ -151,16 +106,7 @@ async def delete_files_controller( dependencies=[Depends(AuthPermission(["module_monitor:resource:move"]))], ) async def move_file_controller(data: ResourceMoveSchema) -> JSONResponse: - """ - 移动文件 - - 参数: - - data (ResourceMoveSchema): 移动文件参数模型。 - - 返回: - - JSONResponse: 包含移动结果的JSON响应。 - """ - await ResourceService.move_file_service(data=data) + await ResourceService.move_file(data=data) return SuccessResponse(msg="移动文件成功") @@ -171,16 +117,7 @@ async def move_file_controller(data: ResourceMoveSchema) -> JSONResponse: dependencies=[Depends(AuthPermission(["module_monitor:resource:copy"]))], ) async def copy_file_controller(data: ResourceCopySchema) -> JSONResponse: - """ - 复制文件 - - 参数: - - data (ResourceCopySchema): 复制文件参数模型。 - - 返回: - - JSONResponse: 包含复制结果的JSON响应。 - """ - await ResourceService.copy_file_service(data=data) + await ResourceService.copy_file(data=data) return SuccessResponse(msg="复制文件成功") @@ -191,16 +128,7 @@ async def copy_file_controller(data: ResourceCopySchema) -> JSONResponse: dependencies=[Depends(AuthPermission(["module_monitor:resource:rename"]))], ) async def rename_file_controller(data: ResourceRenameSchema) -> JSONResponse: - """ - 重命名文件 - - 参数: - - data (ResourceRenameSchema): 重命名文件参数模型。 - - 返回: - - JSONResponse: 包含重命名结果的JSON响应。 - """ - await ResourceService.rename_file_service(data=data) + await ResourceService.rename_file(data=data) return SuccessResponse(msg="重命名文件成功") @@ -213,16 +141,7 @@ async def rename_file_controller(data: ResourceRenameSchema) -> JSONResponse: async def create_directory_controller( data: ResourceCreateDirSchema, ) -> JSONResponse: - """ - 创建目录 - - 参数: - - data (ResourceCreateDirSchema): 创建目录参数模型。 - - 返回: - - JSONResponse: 包含创建目录结果的JSON响应。 - """ - await ResourceService.create_directory_service(data=data) + await ResourceService.create_directory(data=data) return SuccessResponse(msg="创建目录成功") @@ -233,19 +152,8 @@ async def create_directory_controller( dependencies=[Depends(AuthPermission(["module_monitor:resource:export"]))], ) async def export_resource_list_controller(request: Request, search: Annotated[ResourceSearchQueryParam, Depends()]) -> StreamingResponse: - """ - 导出资源列表 - - 参数: - - request (Request): FastAPI请求对象,用于获取基础URL。 - - search (ResourceSearchQueryParam): 资源查询参数模型。 - - 返回: - - StreamingResponse: 包含导出资源列表的流式响应。 - """ - # 获取搜索结果 - result_dict_list = await ResourceService.get_resources_list_service(search=search, base_url=str(request.base_url)) - export_result = await ResourceService.export_resource_service(data_list=result_dict_list) + result_dict_list = await ResourceService.get_resources_list(search=search, base_url=str(request.base_url)) + export_result = await ResourceService.export_resource(data_list=result_dict_list) return StreamResponse( data=bytes2file_response(export_result), diff --git a/backend/app/api/v1/module_monitor/resource/service.py b/backend/app/api/v1/module_monitor/resource/service.py index 9b07cf52..3a9b885a 100644 --- a/backend/app/api/v1/module_monitor/resource/service.py +++ b/backend/app/api/v1/module_monitor/resource/service.py @@ -24,66 +24,36 @@ from .schema import ( class ResourceService: - """ - 资源管理模块服务层 - 管理系统静态文件目录(仅管理 upload 目录) - """ + """资源管理模块服务层 - 管理系统静态文件目录(仅管理 upload 目录)""" - # 配置常量 MAX_UPLOAD_SIZE = 100 * 1024 * 1024 # 100MB - MAX_SEARCH_RESULTS = 1000 # 最大搜索结果数 - MAX_PATH_DEPTH = 20 # 最大路径深度 + MAX_SEARCH_RESULTS = 1000 + MAX_PATH_DEPTH = 20 - @classmethod - def _get_resource_root(cls) -> str: - """ - 获取资源管理根目录(仅允许访问 upload/resource 目录) - - 返回: - - str: 资源管理根目录路径(upload/resource 目录)。 - """ + @staticmethod + def _get_resource_root() -> str: if not settings.STATIC_ENABLE: raise CustomException(msg="静态文件服务未启用") - # 限制只能管理 upload/resource 目录(与 upload_type="resource" 保持一致) resource_root = os.path.join(str(settings.STATIC_ROOT), "upload", "resource") - # 确保 resource 目录存在 os.makedirs(resource_root, exist_ok=True) return resource_root - @classmethod - def _get_safe_path(cls, path: str | None = None) -> str: - """ - 获取安全的文件路径(加强版路径遍历防护) - - 参数: - - path (str | None): 原始文件路径。 - - 返回: - - str: 安全的文件路径。 - """ - resource_root = cls._get_resource_root() + @staticmethod + def _get_safe_path(path: str | None = None) -> str: + resource_root = ResourceService._get_resource_root() if not path or not isinstance(path, str): return resource_root - # 支持前端传递的完整URL或以STATIC_URL/ROOT_PATH+STATIC_URL开头的URL路径,转换为相对资源路径 static_prefix = settings.STATIC_URL.rstrip("/") root_prefix = settings.ROOT_PATH.rstrip("/") if getattr(settings, "ROOT_PATH", "") else "" root_static_prefix = f"{root_prefix}{static_prefix}" if root_prefix else static_prefix def strip_prefix(p: str) -> str: - """ - 去掉静态资源 URL 前缀,得到相对 upload 的路径片段。 - - 参数: - - p (str): 原始路径或 URL 路径段。 - - 返回: - - str: 去掉已知前缀后的路径。 - """ if p.startswith(root_static_prefix): - return p[len(root_static_prefix) :].lstrip("/") + return p[len(root_static_prefix):].lstrip("/") if p.startswith(static_prefix): - return p[len(static_prefix) :].lstrip("/") + return p[len(static_prefix):].lstrip("/") return p if path.startswith(("http://", "https://")): @@ -93,98 +63,66 @@ class ResourceService: else: path = strip_prefix(path) - # 清理路径,规范化斜杠 path = path.strip().replace("//", "/").replace("\\\\\\\\", "/").replace("\\\\", "/") - # 移除开头的 /,将路径视为相对于 resource_root if path.startswith("/"): path = path[1:] - # 如果路径以 upload/ 开头,去掉 upload/ 前缀 - # 因为 _get_resource_root() 已经指向了 upload 目录 if path.startswith("upload/"): - path = path[7:] # len("upload/") = 7 + path = path[7:] - # 检查路径遍历攻击 if ".." in path or "\x00" in path: logger.error(f"检测到路径遍历攻击尝试: {path}") raise CustomException(msg="非法的路径格式") - # URL 解码检查 decoded_path = urllib.parse.unquote(path) if ".." in decoded_path: logger.error(f"检测到编码后的路径遍历攻击: {path}") raise CustomException(msg="非法的路径格式") - # 构建完整路径 safe_path = os.path.normpath(os.path.join(resource_root, path)) - # 获取绝对路径并规范化 resource_root_abs = os.path.normpath(os.path.abspath(resource_root)) safe_path_abs = os.path.normpath(os.path.abspath(safe_path)) - # 核心安全检查:确保最终路径在允许的根目录下 if not safe_path_abs.startswith(resource_root_abs + os.sep) and safe_path_abs != resource_root_abs: logger.error(f"路径遍历攻击被阻止: 尝试访问 {safe_path_abs}, 但根目录是 {resource_root_abs}") raise CustomException(msg="访问路径不在允许范围内") - # 检查路径深度 try: relative_path = os.path.relpath(safe_path_abs, resource_root_abs) - if relative_path.count(os.sep) > cls.MAX_PATH_DEPTH: + if relative_path.count(os.sep) > ResourceService.MAX_PATH_DEPTH: raise CustomException(msg="路径深度超过限制") except ValueError: raise CustomException(msg="无效的路径") return safe_path_abs - @classmethod - def _path_exists(cls, path: str) -> bool: - """ - 检查路径是否存在 - - 参数: - - path (str): 要检查的路径。 - - 返回: - - bool: 如果路径存在则返回True,否则返回False。 - """ + @staticmethod + def _path_exists(path: str) -> bool: try: - safe_path = cls._get_safe_path(path) + safe_path = ResourceService._get_safe_path(path) return os.path.exists(safe_path) except Exception as e: raise CustomException(msg=f"检查路径是否存在失败: {e!s}") @staticmethod def _sanitize_filename(filename: str) -> str: - """ - 清理文件名,移除危险字符和路径穿越(加强版)。 - - 参数: - - filename (str): 原始文件名。 - - 返回: - - str: 安全的文件名。 - """ if not filename: return f"file_{datetime.now().strftime('%Y%m%d%H%M%S')}" - # 首先检查原始文件名是否包含路径遍历特征 - # 攻击者可能使用 ..\..\etc\passwd 或 ../../etc/passwd dangerous_patterns = [ - r"\.\.", # .. 路径遍历 - r"[\/]", # 任何斜杠(目录分隔符) - r"\x00", # 空字节 - r"%2e%2e", # URL 编码的 .. - r"%252e%252e", # 双重 URL 编码的 .. + r"\.\.", + r"[\/]", + r"\x00", + r"%2e%2e", + r"%252e%252e", ] for pattern in dangerous_patterns: if re.search(pattern, filename, re.IGNORECASE): logger.error(f"检测到文件名路径遍历攻击: {filename}") - # 返回安全文件名,不包含原始文件名 return f"file_{datetime.now().strftime('%Y%m%d%H%M%S')}" - # URL 解码检查 decoded = urllib.parse.unquote(filename) decoded_twice = urllib.parse.unquote(decoded) for check in [decoded, decoded_twice]: @@ -192,19 +130,11 @@ class ResourceService: logger.error(f"检测到编码后的文件名攻击: {filename}") return f"file_{datetime.now().strftime('%Y%m%d%H%M%S')}" - # 使用 os.path.basename 提取纯文件名(移除路径) filename = os.path.basename(filename) - - # 移除危险字符 filename = re.sub(r'[<>:"|?*\x00-\x1f]', "", filename) - - # 防止多个连续点号(可能被用于绕过扩展名检查) filename = re.sub(r"\.{2,}", ".", filename) - - # 移除首尾的空格和点号 filename = filename.strip(". ") - # 如果文件名为空,生成默认文件名 if not filename: filename = f"file_{datetime.now().strftime('%Y%m%d%H%M%S')}" @@ -212,15 +142,6 @@ class ResourceService: @staticmethod def _detect_file_type(content: bytes) -> str | None: - """ - 通过文件内容检测真实文件类型。 - - 参数: - - content (bytes): 文件内容(前几字节即可)。 - - 返回: - - str | None: 检测到的 MIME 类型,无法识别返回 None。 - """ if content.startswith(b"\xff\xd8\xff"): return "image/jpeg" if content.startswith(b"\x89PNG\r\n\x1a\n"): @@ -237,54 +158,27 @@ class ResourceService: return "application/msword" return None - @classmethod - def _generate_http_url(cls, file_path: str, base_url: str | None = None) -> str: - """ - 生成文件的HTTP URL - - 参数: - - file_path (str): 文件的绝对路径。 - - base_url (str | None): 基础URL,用于生成完整URL。 - - 返回: - - str: 文件的HTTP URL。 - """ - # 使用 STATIC_ROOT 作为基准,而不是 _get_resource_root() - # 这样可以保留 upload 目录在 URL 中 + @staticmethod + def _generate_http_url(file_path: str, base_url: str | None = None) -> str: static_root = str(settings.STATIC_ROOT) try: relative_path = os.path.relpath(file_path, static_root) - # 确保路径使用正斜杠(URL格式) url_path = relative_path.replace(os.sep, "/") except ValueError: - # 如果无法计算相对路径,使用文件名 url_path = os.path.basename(file_path) - # 如果提供了base_url,使用它生成完整URL,否则使用settings.STATIC_URL if base_url: - # 使用完整的 base_url(包含 API 路径前缀) base_part = base_url.rstrip("/") static_part = settings.STATIC_URL.lstrip("/") file_part = url_path.lstrip("/") - http_url = f"{base_part}/{static_part}/{file_part}".replace("//", "/").replace(":/", "://") else: http_url = f"{settings.STATIC_URL}/{url_path}".replace("//", "/") return http_url - @classmethod - def _get_file_info(cls, file_path: str, base_url: str | None = None) -> ResourceItemSchema | None: - """ - 获取文件或目录的详细信息。 - - 参数: - - file_path (str): 文件或目录的路径(必须是绝对路径)。 - - base_url (str | None): 基础URL,用于生成完整URL。 - - 返回: - - ResourceItemSchema | None: 文件或目录的详细信息,路径不存在时返回 None。 - """ + @staticmethod + def _get_file_info(file_path: str, base_url: str | None = None) -> ResourceItemSchema | None: try: safe_path = file_path if not os.path.exists(safe_path): @@ -292,14 +186,14 @@ class ResourceService: stat = os.stat(safe_path) path_obj = Path(safe_path) - resource_root = cls._get_resource_root() + resource_root = ResourceService._get_resource_root() try: relative_path = os.path.relpath(safe_path, resource_root) except ValueError: relative_path = os.path.basename(safe_path) - http_url = cls._generate_http_url(safe_path, base_url) + http_url = ResourceService._generate_http_url(safe_path, base_url) is_hidden = path_obj.name.startswith(".") return ResourceItemSchema( @@ -317,32 +211,19 @@ class ResourceService: logger.error(f"获取文件信息失败: {e!s}") return None - @classmethod - async def get_directory_list_service( - cls, + @staticmethod + async def get_directory_list( path: str | None = None, include_hidden: bool = False, base_url: str | None = None, ) -> ResourceDirectorySchema: - """ - 获取目录列表 - - 参数: - - path (str | None): 目录路径。如果未指定,将使用静态文件根目录。 - - include_hidden (bool): 是否包含隐藏文件。 - - base_url (str | None): 基础URL,用于生成完整URL。 - - 返回: - - dict: 包含目录列表和统计信息的字典。 - """ try: - # 如果没有指定路径,使用静态文件根目录 if path is None: - safe_path = cls._get_resource_root() - display_path = cls._generate_http_url(safe_path, base_url) + safe_path = ResourceService._get_resource_root() + display_path = ResourceService._generate_http_url(safe_path, base_url) else: - safe_path = cls._get_safe_path(path) - display_path = cls._generate_http_url(safe_path, base_url) + safe_path = ResourceService._get_safe_path(path) + display_path = ResourceService._generate_http_url(safe_path, base_url) if not os.path.exists(safe_path): raise CustomException(msg="目录不存在") @@ -357,16 +238,14 @@ class ResourceService: try: for item_name in os.listdir(safe_path): - # 跳过隐藏文件 if not include_hidden and item_name.startswith("."): continue item_path = os.path.join(safe_path, item_name) - file_info = cls._get_file_info(item_path, base_url) + file_info = ResourceService._get_file_info(item_path, base_url) if file_info: items.append(file_info) - if file_info.is_file: total_files += 1 total_size += file_info.size or 0 @@ -391,68 +270,48 @@ class ResourceService: logger.error(f"获取目录列表失败: {e!s}") raise CustomException(msg=f"获取目录列表失败: {e!s}") - @classmethod - async def get_resources_list_service( - cls, + @staticmethod + async def get_resources_list( search: ResourceSearchQueryParam | None = None, order_by: str | None = None, base_url: str | None = None, ) -> list[ResourceItemSchema]: - """ - 搜索资源列表(用于分页和导出) - - 参数: - - search (ResourceSearchQueryParam | None): 查询参数模型。 - - order_by (str | None): 排序参数。 - - base_url (str | None): 基础URL,用于生成完整URL。 - - 返回: - - list[ResourceItemSchema]: 资源详情列表。 - """ try: - # 确定搜索路径 if search and hasattr(search, "path") and search.path and isinstance(search.path, str): - resource_root = cls._get_safe_path(search.path) + resource_root = ResourceService._get_safe_path(search.path) else: - resource_root = cls._get_resource_root() + resource_root = ResourceService._get_resource_root() - # 检查路径是否存在 if not os.path.exists(resource_root): raise CustomException(msg="目录不存在") if not os.path.isdir(resource_root): raise CustomException(msg="路径不是目录") - # 收集资源 all_resources = [] try: for item_name in os.listdir(resource_root): - # 跳过隐藏文件 if item_name.startswith("."): continue item_path = os.path.join(resource_root, item_name) - file_info = cls._get_file_info(item_path, base_url) + file_info = ResourceService._get_file_info(item_path, base_url) if file_info: - # 应用名称过滤 if search and hasattr(search, "name") and search.name and search.name[1]: search_keyword = search.name[1].lower() if search_keyword not in file_info.name.lower(): continue - all_resources.append(file_info) except PermissionError: raise CustomException(msg="没有权限访问此目录") - # 应用排序 - sorted_resources = cls._sort_results(all_resources, order_by) + sorted_resources = ResourceService._sort_results(all_resources, order_by) - # 限制最大结果数 - if len(sorted_resources) > cls.MAX_SEARCH_RESULTS: - sorted_resources = sorted_resources[: cls.MAX_SEARCH_RESULTS] + if len(sorted_resources) > ResourceService.MAX_SEARCH_RESULTS: + sorted_resources = sorted_resources[: ResourceService.MAX_SEARCH_RESULTS] return sorted_resources @@ -460,17 +319,8 @@ class ResourceService: logger.error(f"搜索资源失败: {e!s}") raise CustomException(msg=f"搜索资源失败: {e!s}") - @classmethod - async def export_resource_service(cls, data_list: list[ResourceItemSchema]) -> bytes: - """ - 导出资源列表 - - 参数: - - data_list (list[ResourceItemSchema]): 资源详情列表。 - - 返回: - - bytes: Excel文件的二进制数据。 - """ + @staticmethod + async def export_resource(data_list: list[ResourceItemSchema]) -> bytes: mapping_dict = { "name": "文件名", "path": "文件路径", @@ -480,33 +330,148 @@ class ResourceService: "parent_path": "父目录", } - # 复制数据并转换状态 export_data = [item.model_dump() for item in data_list] - # 格式化文件大小 for item in export_data: if item.get("size"): - item["size"] = cls._format_file_size(item["size"]) + item["size"] = ResourceService._format_file_size(item["size"]) return ExcelUtil.export_list2excel(list_data=export_data, mapping_dict=mapping_dict) - @classmethod - async def _get_directory_stats(cls, path: str, include_hidden: bool = False) -> dict[str, int]: - """ - 递归获取目录统计信息 + @staticmethod + async def download_file(file_path: str) -> str: + safe_path = ResourceService._get_safe_path(file_path) + if not os.path.exists(safe_path): + raise CustomException(msg="文件不存在") + if not os.path.isfile(safe_path): + raise CustomException(msg="路径不是文件") + return safe_path - 参数: - - path (str): 目录路径。 - - include_hidden (bool): 是否包含隐藏文件。 + @staticmethod + async def delete_file(paths: list[str]) -> None: + for path_item in paths: + safe_path = ResourceService._get_safe_path(path_item) - 返回: - - dict[str, int]: 包含文件数、目录数和总大小的字典。 - """ + if not os.path.exists(safe_path): + raise CustomException(msg=f"文件不存在: {path_item}") + + try: + if os.path.isfile(safe_path): + os.remove(safe_path) + elif os.path.isdir(safe_path): + shutil.rmtree(safe_path) + else: + raise CustomException(msg=f"无法识别的文件类型: {path_item}") + except PermissionError: + raise CustomException(msg=f"没有权限删除: {path_item}") + except OSError as e: + raise CustomException(msg=f"删除失败: {path_item} - {e!s}") + + logger.info(f"成功删除: {path_item}") + + @staticmethod + async def move_file(data: ResourceMoveSchema) -> None: + source_safe = ResourceService._get_safe_path(data.source_path) + target_dir_safe = ResourceService._get_safe_path(data.target_dir) + + if not os.path.exists(source_safe): + raise CustomException(msg=f"源文件不存在: {data.source_path}") + + if not os.path.isdir(target_dir_safe): + raise CustomException(msg=f"目标目录不存在: {data.target_dir}") + + filename = os.path.basename(source_safe) + target_path = os.path.join(target_dir_safe, filename) + + if os.path.exists(target_path): + raise CustomException(msg=f"目标位置已存在同名文件: {filename}") + + try: + shutil.move(source_safe, target_path) + except PermissionError: + raise CustomException(msg=f"没有权限移动文件: {data.source_path}") + except OSError as e: + raise CustomException(msg=f"移动文件失败: {e!s}") + + logger.info(f"成功移动文件: {data.source_path} -> {data.target_dir}") + + @staticmethod + async def copy_file(data: ResourceCopySchema) -> None: + source_safe = ResourceService._get_safe_path(data.source_path) + target_dir_safe = ResourceService._get_safe_path(data.target_dir) + + if not os.path.exists(source_safe): + raise CustomException(msg=f"源文件不存在: {data.source_path}") + + if not os.path.isdir(target_dir_safe): + raise CustomException(msg=f"目标目录不存在: {data.target_dir}") + + filename = os.path.basename(source_safe) + target_path = os.path.join(target_dir_safe, filename) + + if os.path.exists(target_path): + raise CustomException(msg=f"目标位置已存在同名文件: {filename}") + + try: + if os.path.isdir(source_safe): + shutil.copytree(source_safe, target_path) + else: + shutil.copy2(source_safe, target_path) + except PermissionError: + raise CustomException(msg=f"没有权限复制文件: {data.source_path}") + except OSError as e: + raise CustomException(msg=f"复制文件失败: {e!s}") + + logger.info(f"成功复制文件: {data.source_path} -> {data.target_dir}") + + @staticmethod + async def rename_file(data: ResourceRenameSchema) -> None: + safe_path = ResourceService._get_safe_path(data.file_path) + parent_dir = os.path.dirname(safe_path) + safe_name = ResourceService._sanitize_filename(data.new_name) + + new_path = os.path.join(parent_dir, safe_name) + + if os.path.exists(new_path): + raise CustomException(msg=f"目标文件名已存在: {safe_name}") + + try: + os.rename(safe_path, new_path) + except PermissionError: + raise CustomException(msg=f"没有权限重命名: {data.file_path}") + except OSError as e: + raise CustomException(msg=f"重命名失败: {e!s}") + + logger.info(f"成功重命名: {data.file_path} -> {safe_name}") + + @staticmethod + async def create_directory(data: ResourceCreateDirSchema) -> None: + parent_dir = ResourceService._get_safe_path(data.parent_path) + + if not os.path.isdir(parent_dir): + raise CustomException(msg=f"父目录不存在: {data.parent_path}") + + safe_name = ResourceService._sanitize_filename(data.dir_name) + new_dir = os.path.join(parent_dir, safe_name) + + if os.path.exists(new_dir): + raise CustomException(msg=f"目录已存在: {data.dir_name}") + + try: + os.makedirs(new_dir, exist_ok=False) + except PermissionError: + raise CustomException(msg=f"没有权限创建目录: {data.dir_name}") + except OSError as e: + raise CustomException(msg=f"创建目录失败: {e!s}") + + logger.info(f"成功创建目录: {data.parent_path}/{safe_name}") + + @staticmethod + async def _get_directory_stats(path: str, include_hidden: bool = False) -> dict[str, int]: stats = {"files": 0, "dirs": 0, "size": 0} try: for root, dirs, files in os.walk(path): - # 过滤隐藏目录 if not include_hidden: dirs[:] = [d for d in dirs if not d.startswith(".")] files = [f for f in files if not f.startswith(".")] @@ -520,30 +485,17 @@ class ResourceService: stats["size"] += os.path.getsize(file_path) except OSError: continue - except Exception: pass return stats - @classmethod - def _sort_results(cls, results: list[ResourceItemSchema], order_by: str | None = None) -> list[ResourceItemSchema]: - """ - 排序搜索结果 - - 参数: - - results (list[ResourceItemSchema]): 资源详情列表。 - - order_by (str | None): 排序参数。 - - 返回: - - list[ResourceItemSchema]: 排序后的资源详情列表。 - """ + @staticmethod + def _sort_results(results: list[ResourceItemSchema], order_by: str | None = None) -> list[ResourceItemSchema]: try: - # 默认按名称升序排序 if not order_by: return sorted(results, key=lambda x: x.name, reverse=False) - # 解析order_by参数,格式: [{'field':'asc/desc'}] sort_conditions = ast.literal_eval(order_by) if isinstance(sort_conditions, list): @@ -558,323 +510,22 @@ class ResourceService: keys.append(value) return keys - # 确定排序方向(这里只支持单一方向,多个条件时使用第一个条件的方向) reverse = False if sort_conditions and isinstance(sort_conditions[0], dict): - direction = sort_conditions[0].get("direction", "").lower() - reverse = direction == "desc" + order = sort_conditions[0].get("order", "asc") + reverse = order.lower() == "desc" return sorted(results, key=sort_key, reverse=reverse) - # 如果排序条件不是列表,返回默认排序 - return sorted(results, key=lambda x: x.get("name", ""), reverse=False) - - except Exception as e: - raise CustomException(msg=f"排序参数格式错误: {e!s}") - - @classmethod - async def download_file_service(cls, file_path: str) -> str: - """ - 下载文件(返回本地文件系统路径) - - 参数: - - file_path (str): 文件路径(可为相对路径、绝对路径或完整URL)。 - - 返回: - - str: 本地文件系统路径。 - """ - try: - safe_path = cls._get_safe_path(file_path) - - if not os.path.exists(safe_path): - raise CustomException(msg="文件不存在") - - if not os.path.isfile(safe_path): - raise CustomException(msg="路径不是文件") - - # 返回本地文件路径给 FileResponse 使用 - return safe_path - - except CustomException: - raise - except Exception as e: - logger.error(f"下载文件失败: {e!s}") - raise CustomException(msg=f"下载文件失败: {e!s}") - - @classmethod - def _delete_single_path(cls, path: str) -> None: - """ - 删除单个文件或目录(内部辅助方法) - - 参数: - - path (str): 文件或目录路径。 - - 返回: - - None - - 异常: - - CustomException: 删除失败时抛出 - """ - safe_path = cls._get_safe_path(path) - - if not os.path.exists(safe_path): - logger.error(f"路径不存在,跳过: {path}") - raise CustomException(msg=f"路径不存在: {path}") - - if os.path.isfile(safe_path): - os.remove(safe_path) - elif os.path.isdir(safe_path): - shutil.rmtree(safe_path) - - @classmethod - async def delete_file_service(cls, paths: list[str]) -> None: - """ - 删除文件或目录(内部使用,遇到错误会抛出异常) - - 参数: - - paths (list[str]): 文件或目录路径列表。 - - 返回: - - None - - 注意: - - 此方法遇到第一个错误就会抛出异常并停止 - - 如需批量删除并收集结果,请使用 batch_delete_service - """ - if not paths: - raise CustomException(msg="删除失败,删除路径不能为空") - - for path in paths: - try: - cls._delete_single_path(path) - except Exception as e: - logger.error(f"删除失败 {path}: {e!s}") - raise CustomException(msg=f"删除失败 {path}: {e!s}") - - @classmethod - async def batch_delete_service(cls, paths: list[str]) -> dict[str, list[str]]: - """ - 批量删除文件或目录 - - 参数: - - paths (list[str]): 文件或目录路径列表。 - - 返回: - - dict[str, list[str]]: 键 `success` / `failed` 对应成功与失败路径列表。 - """ - if not paths: - raise CustomException(msg="删除失败,删除路径不能为空") - - success_paths = [] - failed_paths = [] - - for path in paths: - try: - cls._delete_single_path(path) - success_paths.append(path) - except Exception: - failed_paths.append(path) - - return {"success": success_paths, "failed": failed_paths} - - @classmethod - async def move_file_service(cls, data: ResourceMoveSchema) -> None: - """ - 移动文件或目录 - - 参数: - - data (ResourceMoveSchema): 包含源路径和目标路径的模型。 - - 返回: - - None - """ - try: - source_path = cls._get_safe_path(data.source_path) - target_path = cls._get_safe_path(data.target_path) - - if not os.path.exists(source_path): - raise CustomException(msg="源路径不存在") - - # 检查目标路径是否已存在 - if os.path.exists(target_path): - if not data.overwrite: - raise CustomException(msg="目标路径已存在") - # 删除目标路径 - if os.path.isfile(target_path): - os.remove(target_path) - else: - shutil.rmtree(target_path) - - # 确保目标目录存在 - target_dir = os.path.dirname(target_path) - os.makedirs(target_dir, exist_ok=True) - - # 移动文件 - shutil.move(source_path, target_path) - - except CustomException: - raise - except Exception as e: - logger.error(f"移动失败: {e!s}") - raise CustomException(msg=f"移动失败: {e!s}") - - @classmethod - async def copy_file_service(cls, data: ResourceCopySchema) -> None: - """ - 复制文件或目录 - - 参数: - - data (ResourceCopySchema): 包含源路径和目标路径的模型。 - - 返回: - - None - """ - try: - source_path = cls._get_safe_path(data.source_path) - target_path = cls._get_safe_path(data.target_path) - - if not os.path.exists(source_path): - raise CustomException(msg="源路径不存在") - - # 检查目标路径是否已存在 - if os.path.exists(target_path) and not data.overwrite: - raise CustomException(msg="目标路径已存在") - - # 确保目标目录存在 - target_dir = os.path.dirname(target_path) - os.makedirs(target_dir, exist_ok=True) - - # 复制文件或目录 - if os.path.isfile(source_path): - shutil.copy2(source_path, target_path) - else: - shutil.copytree(source_path, target_path, dirs_exist_ok=data.overwrite) - - except CustomException: - raise - except Exception as e: - logger.error(f"复制失败: {e!s}") - raise CustomException(msg=f"复制失败: {e!s}") - - @classmethod - async def rename_file_service(cls, data: ResourceRenameSchema) -> None: - """ - 重命名文件或目录 - - 参数: - - data (ResourceRenameSchema): 包含旧路径和新名称的模型。 - - 返回: - - None - """ - try: - old_path = cls._get_safe_path(data.old_path) - - if not os.path.exists(old_path): - raise CustomException(msg="文件或目录不存在") - - # 清理新名称,防止路径遍历 - # 使用 _sanitize_filename 来清理,确保不包含路径分隔符 - safe_new_name = cls._sanitize_filename(data.new_name) - - # 如果新名称被重置,说明检测到攻击 - if safe_new_name.startswith("file_") and safe_new_name != data.new_name: - logger.error(f"重命名时检测到路径遍历攻击,原始名称: {data.new_name}") - raise CustomException(msg="新名称包含非法字符") - - # 生成新路径 - parent_dir = os.path.dirname(old_path) - new_path = os.path.join(parent_dir, safe_new_name) - - # 最终安全检查:确保新路径在允许的目录下 - new_path_abs = os.path.normpath(os.path.abspath(new_path)) - resource_root_abs = os.path.normpath(os.path.abspath(cls._get_resource_root())) - - if not new_path_abs.startswith(resource_root_abs + os.sep) and new_path_abs != resource_root_abs: - logger.error(f"重命名时检测到越权访问: {new_path_abs}") - raise CustomException(msg="目标路径不在允许范围内") - - if os.path.exists(new_path): - raise CustomException(msg="目标名称已存在") - - # 重命名 - os.rename(old_path, new_path) - - except CustomException: - raise - except Exception as e: - logger.error(f"重命名失败: {e!s}") - raise CustomException(msg=f"重命名失败: {e!s}") - - @classmethod - async def create_directory_service(cls, data: ResourceCreateDirSchema) -> None: - """ - 创建目录 - - 参数: - - data (ResourceCreateDirSchema): 包含父目录路径和目录名称的模型。 - - 返回: - - None - """ - try: - parent_path = cls._get_safe_path(data.parent_path) - - if not os.path.exists(parent_path): - raise CustomException(msg="父目录不存在") - - if not os.path.isdir(parent_path): - raise CustomException(msg="父路径不是目录") - - # 清理目录名称,防止路径遍历(使用与文件名相同的清理逻辑) - safe_dir_name = cls._sanitize_filename(data.dir_name) - - # 如果目录名被重置,说明检测到攻击 - if safe_dir_name.startswith("file_") and safe_dir_name != data.dir_name: - logger.error(f"创建目录时检测到路径遍历攻击,原始名称: {data.dir_name}") - raise CustomException(msg="目录名称包含非法字符") - - # 生成新目录路径 - new_dir_path = os.path.join(parent_path, safe_dir_name) - - # 最终安全检查:确保新目录路径在允许的目录下 - new_dir_path_abs = os.path.normpath(os.path.abspath(new_dir_path)) - resource_root_abs = os.path.normpath(os.path.abspath(cls._get_resource_root())) - - if not new_dir_path_abs.startswith(resource_root_abs + os.sep) and new_dir_path_abs != resource_root_abs: - logger.error(f"创建目录时检测到越权访问: {new_dir_path_abs}") - raise CustomException(msg="目标路径不在允许范围内") - - if os.path.exists(new_dir_path): - raise CustomException(msg="目录已存在") - - # 创建目录 - os.makedirs(new_dir_path) - - except CustomException: - raise - except Exception as e: - logger.error(f"创建目录失败: {e!s}") - raise CustomException(msg=f"创建目录失败: {e!s}") - - @classmethod - def _format_file_size(cls, size_bytes: int) -> str: - """ - 格式化文件大小 - - 参数: - - size_bytes (int): 文件大小(字节) - - 返回: - - str: 格式化后的文件大小字符串(例如:"123.45MB") - """ - if size_bytes == 0: - return "0B" - - size_names = ["B", "KB", "MB", "GB", "TB"] - i = 0 - while size_bytes >= 1024 and i < len(size_names) - 1: - size_bytes = int(size_bytes / 1024) - i += 1 - - return f"{size_bytes:.2f}{size_names[i]}" + return sorted(results, key=lambda x: x.name, reverse=False) + + except (ValueError, SyntaxError): + return sorted(results, key=lambda x: x.name, reverse=False) + + @staticmethod + def _format_file_size(size_bytes: int) -> str: + for unit in ["B", "KB", "MB", "GB"]: + if size_bytes < 1024: + return f"{size_bytes:.2f} {unit}" + size_bytes /= 1024 + return f"{size_bytes:.2f} TB" diff --git a/backend/app/api/v1/module_monitor/server/controller.py b/backend/app/api/v1/module_monitor/server/controller.py index 2c77340c..fb7ae3cb 100644 --- a/backend/app/api/v1/module_monitor/server/controller.py +++ b/backend/app/api/v1/module_monitor/server/controller.py @@ -8,7 +8,7 @@ from app.core.router_class import OperationLogRoute from .service import ServerService -ServerRouter = APIRouter(route_class=OperationLogRoute, prefix="/server", tags=["服务器监控"]) +ServerRouter = APIRouter(route_class=OperationLogRoute, prefix="/server", tags=["系统监控", "服务器监控"]) @ServerRouter.get( @@ -18,12 +18,5 @@ ServerRouter = APIRouter(route_class=OperationLogRoute, prefix="/server", tags=[ response_model=ResponseSchema[ServerMonitorSchema], ) async def get_monitor_server_info_controller() -> JSONResponse: - """ - 查询服务器监控信息 - - 返回: - - JSONResponse: 包含服务器监控信息的JSON响应。 - """ - result_dict = await ServerService.get_server_monitor_info_service() - + result_dict = await ServerService.get_server_monitor_info() return SuccessResponse(data=result_dict, msg="获取服务器监控信息成功") diff --git a/backend/app/api/v1/module_monitor/server/service.py b/backend/app/api/v1/module_monitor/server/service.py index dcb351e1..bee5ec8a 100644 --- a/backend/app/api/v1/module_monitor/server/service.py +++ b/backend/app/api/v1/module_monitor/server/service.py @@ -20,30 +20,18 @@ from .schema import ( class ServerService: """服务监控模块服务层""" - @classmethod - async def get_server_monitor_info_service(cls) -> ServerMonitorSchema: - """ - 获取服务器监控信息 - - 返回: - - Dict: 包含服务器监控信息的字典。 - """ + @staticmethod + async def get_server_monitor_info() -> ServerMonitorSchema: return ServerMonitorSchema( - cpu=cls._get_cpu_info(), - mem=cls._get_memory_info(), - sys=cls._get_system_info(), - py=cls._get_python_info(), - disks=cls._get_disk_info(), + cpu=ServerService._get_cpu_info(), + mem=ServerService._get_memory_info(), + sys=ServerService._get_system_info(), + py=ServerService._get_python_info(), + disks=ServerService._get_disk_info(), ) - @classmethod - def _get_cpu_info(cls) -> CpuInfoSchema: - """ - 获取CPU信息 - - 返回: - - CpuInfoSchema: CPU信息模型。 - """ + @staticmethod + def _get_cpu_info() -> CpuInfoSchema: cpu_times = psutil.cpu_times_percent() cpu_num = psutil.cpu_count(logical=True) if not cpu_num: @@ -55,14 +43,8 @@ class ServerService: free=cpu_times.idle, ) - @classmethod - def _get_memory_info(cls) -> MemoryInfoSchema: - """ - 获取内存信息 - - 返回: - - MemoryInfoSchema: 内存信息模型。 - """ + @staticmethod + def _get_memory_info() -> MemoryInfoSchema: memory = psutil.virtual_memory() return MemoryInfoSchema( total=bytes2human(memory.total), @@ -71,14 +53,8 @@ class ServerService: usage=memory.percent, ) - @classmethod - def _get_system_info(cls) -> SysInfoSchema: - """ - 获取系统信息 - - 返回: - - SysInfoSchema: 系统信息模型。 - """ + @staticmethod + def _get_system_info() -> SysInfoSchema: hostname = socket.gethostname() return SysInfoSchema( computer_ip=socket.gethostbyname(hostname), @@ -88,14 +64,8 @@ class ServerService: user_dir=str(Path.cwd()), ) - @classmethod - def _get_python_info(cls) -> PyInfoSchema: - """ - 获取Python解释器信息 - - 返回: - - PyInfoSchema: Python解释器信息模型。 - """ + @staticmethod + def _get_python_info() -> PyInfoSchema: current_process = psutil.Process() memory = psutil.virtual_memory() process_memory = current_process.memory_info() @@ -115,47 +85,30 @@ class ServerService: memory_usage=round((process_memory.rss / memory.available) * 100, 2), ) - @classmethod - def _get_disk_info(cls) -> list[DiskInfoSchema]: - """ - 获取磁盘信息 - - 返回: - - list[DiskInfoSchema]: 磁盘信息模型列表。 - """ + @staticmethod + def _get_disk_info() -> list[DiskInfoSchema]: disk_info = [] for partition in psutil.disk_partitions(): try: - # 使用mountpoint而不是device来获取磁盘使用情况 usage = psutil.disk_usage(partition.mountpoint) mount_point = str(Path(partition.mountpoint)) disk_info.append( DiskInfoSchema( - dir_name=mount_point, # 使用mountpoint替代device + dir_name=mount_point, sys_type_name=partition.fstype, type_name=f"本地固定磁盘({mount_point})", total=bytes2human(usage.total), used=bytes2human(usage.used), free=bytes2human(usage.free), - usage=usage.percent, # 直接使用数字而不是字符串 + usage=usage.percent, ) ) except (PermissionError, FileNotFoundError): - # 明确指定可能的异常 continue return disk_info - @classmethod - def _calculate_run_time(cls, start_time: float) -> str: - """ - 计算运行时间 - - 参数: - - start_time (float): 进程启动时间(时间戳) - - 返回: - - str: 格式化后的运行时间字符串(例如:"1天2小时3分钟") - """ + @staticmethod + def _calculate_run_time(start_time: float) -> str: difference = time.time() - start_time days = int(difference // (24 * 60 * 60)) hours = int((difference % (24 * 60 * 60)) // (60 * 60)) diff --git a/backend/app/api/v1/module_platform/email/controller.py b/backend/app/api/v1/module_platform/email/controller.py index 950b15a3..4a972f5d 100644 --- a/backend/app/api/v1/module_platform/email/controller.py +++ b/backend/app/api/v1/module_platform/email/controller.py @@ -24,8 +24,7 @@ from .schema import ( ) from .service import EmailConfigService, EmailLogService, EmailSendService, EmailTemplateService -EmailRouter = APIRouter(route_class=OperationLogRoute, prefix="/email", tags=["平台管理/邮件服务"]) - +EmailRouter = APIRouter(route_class=OperationLogRoute, prefix="/email", tags=["平台管理", "邮件服务"]) @EmailRouter.get("/config/list", summary="SMTP 配置列表", response_model=ResponseSchema[PageResultSchema[EmailConfigOutSchema]]) async def email_config_list_controller( @@ -33,18 +32,7 @@ async def email_config_list_controller( search: Annotated[EmailConfigQueryParam, Depends()], auth: Annotated[AuthSchema, Depends(AuthPermission(["module_platform:email:query"]))], ): - """ - SMTP 配置列表 - - 参数: - - page (PaginationQueryParam): 分页查询参数。 - - search (EmailConfigQueryParam): 查询筛选参数。 - - 返回: - - SuccessResponse: 包含分页配置列表的 JSON 响应。 - """ - result = await EmailConfigService.page_service( - auth=auth, + result = await EmailConfigService(auth).page( page_no=page.page_no, page_size=page.page_size, search=search, @@ -52,117 +40,54 @@ async def email_config_list_controller( ) return SuccessResponse(data=result, msg="查询成功") - @EmailRouter.get("/config/detail/{id}", summary="SMTP 配置详情", response_model=ResponseSchema[EmailConfigOutSchema]) async def email_config_detail_controller( id: Annotated[int, Path()], auth: Annotated[AuthSchema, Depends(AuthPermission(["module_platform:email:query"]))], ): - """ - SMTP 配置详情 - - 参数: - - id (int): 配置 ID。 - - 返回: - - SuccessResponse: 包含配置详情的 JSON 响应。 - """ - result = await EmailConfigService.detail_service(auth=auth, id=id) + result = await EmailConfigService(auth).detail(id=id) return SuccessResponse(data=result, msg="查询成功") - @EmailRouter.post("/config/create", summary="创建 SMTP 配置", response_model=ResponseSchema[EmailConfigOutSchema]) async def email_config_create_controller( data: EmailConfigCreateSchema, auth: Annotated[AuthSchema, Depends(AuthPermission(["module_platform:email:update"]))], ): - """ - 创建 SMTP 配置 - - 参数: - - data (EmailConfigCreateSchema): 配置创建参数。 - - 返回: - - SuccessResponse: 包含创建后的配置详情的 JSON 响应。 - """ - result = await EmailConfigService.create_service(auth=auth, data=data) + result = await EmailConfigService(auth).create(data=data) return SuccessResponse(data=result, msg="创建成功") - @EmailRouter.put("/config/update/{id}", summary="更新 SMTP 配置", response_model=ResponseSchema[EmailConfigOutSchema]) async def email_config_update_controller( id: Annotated[int, Path()], data: EmailConfigUpdateSchema, auth: Annotated[AuthSchema, Depends(AuthPermission(["module_platform:email:update"]))], ): - """ - 更新 SMTP 配置 - - 参数: - - id (int): 配置 ID。 - - data (EmailConfigUpdateSchema): 配置更新参数。 - - 返回: - - SuccessResponse: 包含更新后的配置详情的 JSON 响应。 - """ - result = await EmailConfigService.update_service(auth=auth, id=id, data=data) + result = await EmailConfigService(auth).update(id=id, data=data) return SuccessResponse(data=result, msg="更新成功") - @EmailRouter.delete("/config/delete", summary="删除 SMTP 配置", response_model=ResponseSchema[None]) async def email_config_delete_controller( ids: Annotated[list[int], Body()], auth: Annotated[AuthSchema, Depends(AuthPermission(["module_platform:email:update"]))], ): - """ - 删除 SMTP 配置 - - 参数: - - ids (list[int]): 配置 ID 列表。 - - 返回: - - SuccessResponse: 删除结果。 - """ - await EmailConfigService.delete_service(auth=auth, ids=ids) + await EmailConfigService(auth).delete(ids=ids) return SuccessResponse(msg="删除成功") - @EmailRouter.post("/config/test", summary="测试 SMTP 连接", response_model=ResponseSchema) async def email_config_test_controller( data: EmailTestSchema, auth: Annotated[AuthSchema, Depends(AuthPermission(["module_platform:email:update"]))], ): - """ - 测试 SMTP 连接 - - 参数: - - data (EmailTestSchema): 测试参数(配置 ID 与目标邮箱)。 - - 返回: - - SuccessResponse: 包含测试结果的 JSON 响应。 - """ - result = await EmailConfigService.test_service(auth=auth, data=data) + result = await EmailConfigService(auth).test(data=data) return SuccessResponse(data=result, msg="测试邮件已发送") - @EmailRouter.get("/template/list", summary="邮件模板列表", response_model=ResponseSchema[PageResultSchema[EmailTemplateOutSchema]]) async def email_template_list_controller( page: Annotated[PaginationQueryParam, Depends()], search: Annotated[EmailTemplateQueryParam, Depends()], auth: Annotated[AuthSchema, Depends(AuthPermission(["module_platform:email:query"]))], ): - """ - 邮件模板列表 - - 参数: - - page (PaginationQueryParam): 分页查询参数。 - - search (EmailTemplateQueryParam): 查询筛选参数。 - - 返回: - - SuccessResponse: 包含分页模板列表的 JSON 响应。 - """ - result = await EmailTemplateService.page_service( - auth=auth, + result = await EmailTemplateService(auth).page( page_no=page.page_no, page_size=page.page_size, search=search, @@ -170,117 +95,54 @@ async def email_template_list_controller( ) return SuccessResponse(data=result, msg="查询成功") - @EmailRouter.get("/template/detail/{id}", summary="邮件模板详情", response_model=ResponseSchema[EmailTemplateOutSchema]) async def email_template_detail_controller( id: Annotated[int, Path()], auth: Annotated[AuthSchema, Depends(AuthPermission(["module_platform:email:query"]))], ): - """ - 邮件模板详情 - - 参数: - - id (int): 模板 ID。 - - 返回: - - SuccessResponse: 包含模板详情的 JSON 响应。 - """ - result = await EmailTemplateService.detail_service(auth=auth, id=id) + result = await EmailTemplateService(auth).detail(id=id) return SuccessResponse(data=result, msg="查询成功") - @EmailRouter.post("/template/create", summary="创建邮件模板", response_model=ResponseSchema[EmailTemplateOutSchema]) async def email_template_create_controller( data: EmailTemplateCreateSchema, auth: Annotated[AuthSchema, Depends(AuthPermission(["module_platform:email:update"]))], ): - """ - 创建邮件模板 - - 参数: - - data (EmailTemplateCreateSchema): 模板创建参数。 - - 返回: - - SuccessResponse: 包含创建后的模板详情的 JSON 响应。 - """ - result = await EmailTemplateService.create_service(auth=auth, data=data) + result = await EmailTemplateService(auth).create(data=data) return SuccessResponse(data=result, msg="创建成功") - @EmailRouter.put("/template/update/{id}", summary="更新邮件模板", response_model=ResponseSchema[EmailTemplateOutSchema]) async def email_template_update_controller( id: Annotated[int, Path()], data: EmailTemplateUpdateSchema, auth: Annotated[AuthSchema, Depends(AuthPermission(["module_platform:email:update"]))], ): - """ - 更新邮件模板 - - 参数: - - id (int): 模板 ID。 - - data (EmailTemplateUpdateSchema): 模板更新参数。 - - 返回: - - SuccessResponse: 包含更新后的模板详情的 JSON 响应。 - """ - result = await EmailTemplateService.update_service(auth=auth, id=id, data=data) + result = await EmailTemplateService(auth).update(id=id, data=data) return SuccessResponse(data=result, msg="更新成功") - @EmailRouter.delete("/template/delete", summary="删除邮件模板", response_model=ResponseSchema[None]) async def email_template_delete_controller( ids: Annotated[list[int], Body()], auth: Annotated[AuthSchema, Depends(AuthPermission(["module_platform:email:update"]))], ): - """ - 删除邮件模板 - - 参数: - - ids (list[int]): 模板 ID 列表。 - - 返回: - - SuccessResponse: 删除结果。 - """ - await EmailTemplateService.delete_service(auth=auth, ids=ids) + await EmailTemplateService(auth).delete(ids=ids) return SuccessResponse(msg="删除成功") - @EmailRouter.post("/send", summary="手动发送邮件(超管测试/补发)", response_model=ResponseSchema) async def email_send_controller( data: EmailSendSchema, auth: Annotated[AuthSchema, Depends(AuthPermission(["module_platform:email:update"]))], ): - """ - 手动发送邮件 - - 参数: - - data (EmailSendSchema): 发送参数(模板代码、目标邮箱、模板变量)。 - - 返回: - - SuccessResponse: 包含发送结果的 JSON 响应。 - """ - result = await EmailSendService.manual_send_service(auth=auth, data=data) + result = await EmailSendService(auth).manual_send(data=data) return SuccessResponse(data=result, msg="发送成功") - @EmailRouter.get("/log/list", summary="邮件发送日志", response_model=ResponseSchema[PageResultSchema[EmailLogOutSchema]]) async def email_log_list_controller( page: Annotated[PaginationQueryParam, Depends()], search: Annotated[EmailLogQueryParam, Depends()], auth: Annotated[AuthSchema, Depends(AuthPermission(["module_platform:email:query"]))], ): - """ - 邮件发送日志 - - 参数: - - page (PaginationQueryParam): 分页查询参数。 - - search (EmailLogQueryParam): 查询筛选参数。 - - 返回: - - SuccessResponse: 包含分页日志列表的 JSON 响应。 - """ - result = await EmailLogService.page_service( - auth=auth, + result = await EmailLogService(auth).page( page_no=page.page_no, page_size=page.page_size, search=search, diff --git a/backend/app/api/v1/module_platform/email/service.py b/backend/app/api/v1/module_platform/email/service.py index be881ef9..7a8f483c 100644 --- a/backend/app/api/v1/module_platform/email/service.py +++ b/backend/app/api/v1/module_platform/email/service.py @@ -1,7 +1,6 @@ from datetime import datetime from app.core.base_schema import AuthSchema -from app.core.database import async_db_session from app.core.dependencies import require_superadmin from app.core.exceptions import CustomException from app.utils.email_util import render_template, send_email @@ -24,39 +23,20 @@ from .schema import ( class EmailConfigService: - """ - 邮件配置服务 - """ - - """ - 邮件配置服务 - """ """SMTP 配置管理(仅超级管理员可操作)""" - @classmethod + def __init__(self, auth: AuthSchema) -> None: + self.auth = auth + @require_superadmin - async def page_service( - cls, - auth: AuthSchema, + async def page( + self, page_no: int, page_size: int, search: EmailConfigQueryParam | None = None, order_by: list | None = None, ) -> dict: - """ - SMTP 配置分页查询 - - 参数: - - auth (AuthSchema): 认证信息模型 - - page_no (int): 页码 - - page_size (int): 每页数量 - - search (EmailConfigQueryParam | None): 查询参数 - - order_by (list | None): 排序参数 - - 返回: - - dict: 分页数据 - """ - return await EmailConfigCRUD(auth).page( + return await EmailConfigCRUD(self.auth).page( offset=(page_no - 1) * page_size, limit=page_size, order_by=order_by or [{"id": "asc"}], @@ -64,99 +44,44 @@ class EmailConfigService: out_schema=EmailConfigOutSchema, ) - @classmethod @require_superadmin - async def detail_service(cls, auth: AuthSchema, id: int) -> EmailConfigOutSchema: - """ - SMTP 配置详情 + async def detail(self, id: int) -> EmailConfigOutSchema: + return await EmailConfigCRUD(self.auth).get_or_404(id=id, out_schema=EmailConfigOutSchema, msg="SMTP 配置不存在") - 参数: - - auth (AuthSchema): 认证信息模型 - - id (int): SMTP 配置 ID - - 返回: - - EmailConfigOutSchema: SMTP 配置详情 - """ - return await EmailConfigCRUD(auth).get_or_404(id=id, out_schema=EmailConfigOutSchema, msg="SMTP 配置不存在") - - @classmethod - async def create_service(cls, auth: AuthSchema, data: EmailConfigCreateSchema) -> EmailConfigOutSchema: - """ - 创建 SMTP 配置 - - 参数: - - auth (AuthSchema): 认证信息模型 - - data (EmailConfigCreateSchema): SMTP 配置创建模型 - - 返回: - - EmailConfigOutSchema: 新创建的 SMTP 配置 - """ - crud = EmailConfigCRUD(auth) + async def create(self, data: EmailConfigCreateSchema) -> EmailConfigOutSchema: + crud = EmailConfigCRUD(self.auth) if data.is_default: await crud.clear_default() obj = await crud.create(data=data) return EmailConfigOutSchema.model_validate(obj) - @classmethod - async def update_service(cls, auth: AuthSchema, id: int, data: EmailConfigUpdateSchema) -> EmailConfigOutSchema: - """ - 更新 SMTP 配置 - - 参数: - - auth (AuthSchema): 认证信息模型 - - id (int): SMTP 配置 ID - - data (EmailConfigUpdateSchema): SMTP 配置更新模型 - - 返回: - - EmailConfigOutSchema: 更新后的 SMTP 配置 - """ - crud = EmailConfigCRUD(auth) + async def update(self, id: int, data: EmailConfigUpdateSchema) -> EmailConfigOutSchema: + crud = EmailConfigCRUD(self.auth) _ = await crud.get_or_404(id=id, msg="SMTP 配置不存在") if data.is_default is True: await crud.clear_default() updated = await crud.update(id=id, data=data) return EmailConfigOutSchema.model_validate(updated) - @classmethod - async def delete_service(cls, auth: AuthSchema, ids: list[int]) -> None: - """ - 删除 SMTP 配置(默认配置不可删除) - - 参数: - - auth (AuthSchema): 认证信息模型 - - ids (list[int]): SMTP 配置 ID 列表 - - 返回: - - None - """ + async def delete(self, ids: list[int]) -> None: if not ids: raise CustomException(msg="删除对象不能为空") - crud = EmailConfigCRUD(auth) + crud = EmailConfigCRUD(self.auth) configs = await crud.list(search={"id": ("in", ids)}) for obj in configs: if obj.is_default: raise CustomException(msg=f"配置「{obj.name}」是默认配置,请先将其他配置设为默认后再删除") await crud.delete(ids=ids) - @classmethod @require_superadmin - async def test_service(cls, auth: AuthSchema, data: EmailTestSchema) -> dict: - """ - 测试 SMTP 连接:发送一封测试邮件 - - 参数: - - auth (AuthSchema): 认证信息模型 - - data (EmailTestSchema): 测试参数(配置 ID 与目标邮箱) - - 返回: - - dict: 测试结果 - """ - config = await EmailConfigCRUD(auth).get(id=data.config_id) + async def test(self, data: EmailTestSchema) -> dict: + config = await EmailConfigCRUD(self.auth).get(id=data.config_id) if not config: raise CustomException(msg="SMTP 配置不存在") - subject = "【FastapiAdmin】SMTP 连接测试" - body_html = ("

这是一封测试邮件,SMTP 配置「{name}」连接成功!

发送时间:{time}

").format(name=config.name, time=datetime.now().strftime("%Y-%m-%d %H:%M:%S")) + subject = ("【FastapiAdmin】SMTP 连接测试") + body_html = ("

这是一封测试邮件,SMTP 配置「{name}」连接成功!

" + "

发送时间:{time}

").format(name=config.name, time=datetime.now().strftime("%Y-%m-%d %H:%M:%S")) try: await send_email( @@ -179,36 +104,20 @@ class EmailConfigService: class EmailTemplateService: - """ - 邮件模板服务 - """ - """邮件模板管理(仅超级管理员可操作)""" - @classmethod + def __init__(self, auth: AuthSchema) -> None: + self.auth = auth + @require_superadmin - async def page_service( - cls, - auth: AuthSchema, + async def page( + self, page_no: int, page_size: int, search: EmailTemplateQueryParam | None = None, order_by: list | None = None, ) -> dict: - """ - 邮件模板分页查询 - - 参数: - - auth (AuthSchema): 认证信息模型 - - page_no (int): 页码 - - page_size (int): 每页数量 - - search (EmailTemplateQueryParam | None): 查询参数 - - order_by (list | None): 排序参数 - - 返回: - - dict: 分页数据 - """ - return await EmailTemplateCRUD(auth).page( + return await EmailTemplateCRUD(self.auth).page( offset=(page_no - 1) * page_size, limit=page_size, order_by=order_by or [{"id": "asc"}], @@ -216,99 +125,39 @@ class EmailTemplateService: out_schema=EmailTemplateOutSchema, ) - @classmethod @require_superadmin - async def detail_service(cls, auth: AuthSchema, id: int) -> EmailTemplateOutSchema: - """ - 邮件模板详情 + async def detail(self, id: int) -> EmailTemplateOutSchema: + return await EmailTemplateCRUD(self.auth).get_or_404(id=id, out_schema=EmailTemplateOutSchema, msg="邮件模板不存在") - 参数: - - auth (AuthSchema): 认证信息模型 - - id (int): 邮件模板 ID - - 返回: - - EmailTemplateOutSchema: 邮件模板详情 - """ - return await EmailTemplateCRUD(auth).get_or_404(id=id, out_schema=EmailTemplateOutSchema, msg="邮件模板不存在") - - @classmethod @require_superadmin - async def create_service(cls, auth: AuthSchema, data: EmailTemplateCreateSchema) -> EmailTemplateOutSchema: - """ - 创建邮件模板 - - 参数: - - auth (AuthSchema): 认证信息模型 - - data (EmailTemplateCreateSchema): 邮件模板创建模型 - - 返回: - - EmailTemplateOutSchema: 新创建的邮件模板 - """ - existing = await EmailTemplateCRUD(auth).get_by_code(template_code=data.template_code) + async def create(self, data: EmailTemplateCreateSchema) -> EmailTemplateOutSchema: + existing = await EmailTemplateCRUD(self.auth).get_by_code(template_code=data.template_code) if existing: raise CustomException(msg=f"模板编码「{data.template_code}」已存在") - obj = await EmailTemplateCRUD(auth).create(data=data) + obj = await EmailTemplateCRUD(self.auth).create(data=data) return EmailTemplateOutSchema.model_validate(obj) - @classmethod @require_superadmin - async def update_service(cls, auth: AuthSchema, id: int, data: EmailTemplateUpdateSchema) -> EmailTemplateOutSchema: - """ - 更新邮件模板 - - 参数: - - auth (AuthSchema): 认证信息模型 - - id (int): 邮件模板 ID - - data (EmailTemplateUpdateSchema): 邮件模板更新模型 - - 返回: - - EmailTemplateOutSchema: 更新后的邮件模板 - """ - _ = await EmailTemplateCRUD(auth).get_or_404(id=id, msg="邮件模板不存在") - - updated = await EmailTemplateCRUD(auth).update(id=id, data=data) + async def update(self, id: int, data: EmailTemplateUpdateSchema) -> EmailTemplateOutSchema: + _ = await EmailTemplateCRUD(self.auth).get_or_404(id=id, msg="邮件模板不存在") + updated = await EmailTemplateCRUD(self.auth).update(id=id, data=data) return EmailTemplateOutSchema.model_validate(updated) - @classmethod @require_superadmin - async def delete_service(cls, auth: AuthSchema, ids: list[int]) -> None: - """ - 删除邮件模板 - - 参数: - - auth (AuthSchema): 认证信息模型 - - ids (list[int]): 邮件模板 ID 列表 - - 返回: - - None - """ + async def delete(self, ids: list[int]) -> None: if not ids: raise CustomException(msg="删除对象不能为空") - await EmailTemplateCRUD(auth).delete(ids=ids) + await EmailTemplateCRUD(self.auth).delete(ids=ids) class EmailSendService: - """ - 邮件发送服务 - """ + """邮件发送服务 — 供其他模块调用""" + def __init__(self, auth: AuthSchema) -> None: + self.auth = auth - """ - 邮件发送服务 - """ - """ - 邮件发送服务 — 供其他模块调用。 - - 设计原则: - 1. 优先使用 is_default=True 的 SMTP 配置,无默认配置时降级为站内信(仅记录日志) - 2. 每次发送都写 platform_email_log - 3. 渲染失败/发送失败均记录 error_msg,不向调用方抛出异常(静默降级) - 4. 使用独立数据库会话:邮件发送失败不因调用方事务回滚而丢失日志 - """ - - @classmethod async def send_by_template( - cls, + self, *, to_email: str, template_code: str, @@ -318,25 +167,7 @@ class EmailSendService: tenant_id: int | None = None, config_id: int | None = None, ) -> bool: - """ - 通过模板发送邮件(核心方法,供内部调用)。 - - 参数: - - to_email (str): 收件人邮箱 - - template_code (str): 模板编码 - - variables (dict): 模板变量 - - biz_type (str): 业务类型 - - to_name (str | None): 收件人姓名 - - tenant_id (int | None): 租户 ID - - config_id (int | None): 指定 SMTP 配置 ID - - 返回: - - bool: True=发送成功,False=失败(已降级记录日志) - """ - # ① 获取模板(独立会话,不影响调用方事务) - async with async_db_session() as session: - auth = AuthSchema(db=session, check_data_scope=False) - template = await EmailTemplateCRUD(auth).get_active_by_code(template_code) + template = await EmailTemplateCRUD(self.auth).get_active_by_code(template_code) if not template: await EmailLogCRUD.create_log( @@ -351,7 +182,6 @@ class EmailSendService: ) return False - # ② 渲染主题 & 正文 try: rendered_subject = render_template(template.subject, variables) rendered_html = render_template(template.body_html, variables) @@ -369,13 +199,10 @@ class EmailSendService: ) return False - # ③ 获取 SMTP 配置(独立会话) - async with async_db_session() as session: - auth = AuthSchema(db=session, check_data_scope=False) - if config_id: - config = await EmailConfigCRUD(auth).get_active_by_id(config_id) - else: - config = await EmailConfigCRUD(auth).get_active_default() + if config_id: + config = await EmailConfigCRUD(self.auth).get_active_by_id(config_id) + else: + config = await EmailConfigCRUD(self.auth).get_active_default() if not config: await EmailLogCRUD.create_log( @@ -390,7 +217,6 @@ class EmailSendService: ) return False - # ④ 发送 try: await send_email( smtp_host=config.smtp_host, @@ -420,7 +246,6 @@ class EmailSendService: ) return False - # ⑤ 写成功日志 await EmailLogCRUD.create_log( config_id=config.id, template_code=template_code, @@ -434,19 +259,8 @@ class EmailSendService: return True - @classmethod - async def manual_send_service(cls, auth: AuthSchema, data: EmailSendSchema) -> dict: - """ - 超管手动触发发送(测试/补发用) - - 参数: - - auth (AuthSchema): 认证信息模型 - - data (EmailSendSchema): 发送参数 - - 返回: - - dict: 发送结果 - """ - success = await cls.send_by_template( + async def manual_send(self, data: EmailSendSchema) -> dict: + success = await self.send_by_template( to_email=data.to_email, template_code=data.template_code, variables=data.variables, @@ -460,38 +274,19 @@ class EmailSendService: class EmailLogService: - """ - 邮件日志服务 - """ - - """ - 邮件日志服务 - """ """邮件日志查询""" - @classmethod - async def page_service( - cls, - auth: AuthSchema, + def __init__(self, auth: AuthSchema) -> None: + self.auth = auth + + async def page( + self, page_no: int, page_size: int, search: EmailLogQueryParam | None = None, order_by: list | None = None, ) -> dict: - """ - 邮件发送日志分页查询 - - 参数: - - auth (AuthSchema): 认证信息模型 - - page_no (int): 页码 - - page_size (int): 每页数量 - - search (EmailLogQueryParam | None): 查询参数 - - order_by (list | None): 排序参数 - - 返回: - - dict: 分页数据 - """ - return await EmailLogCRUD(auth).page( + return await EmailLogCRUD(self.auth).page( offset=(page_no - 1) * page_size, limit=page_size, order_by=order_by or [{"created_time": "desc"}], diff --git a/backend/app/api/v1/module_platform/invoice/controller.py b/backend/app/api/v1/module_platform/invoice/controller.py index e1517bb7..4639bf2a 100644 --- a/backend/app/api/v1/module_platform/invoice/controller.py +++ b/backend/app/api/v1/module_platform/invoice/controller.py @@ -6,10 +6,8 @@ from fastapi.responses import JSONResponse from app.common.response import ResponseSchema, SuccessResponse from app.core.base_schema import AuthSchema, PageResultSchema from app.core.dependencies import AuthPermission -from app.core.exceptions import CustomException from app.core.router_class import OperationLogRoute -from .crud import InvoiceCRUD from .schema import ( InvoiceApplySchema, InvoiceIssueSchema, @@ -19,27 +17,16 @@ from .schema import ( ) from .service import InvoicePlatformService, InvoiceTenantService -TenantInvoiceRouter = APIRouter(prefix="/tenant/invoice", route_class=OperationLogRoute, tags=["发票管理"]) - +TenantInvoiceRouter = APIRouter(prefix="/tenant/invoice", route_class=OperationLogRoute, tags=["平台管理", "发票管理"]) @TenantInvoiceRouter.post("/apply", summary="申请开票", response_model=ResponseSchema[InvoiceOutSchema]) async def invoice_apply_controller( data: Annotated[InvoiceApplySchema, Body()], auth: Annotated[AuthSchema, Depends(AuthPermission(["*:*:*"]))], ) -> JSONResponse: - """ - 申请开票 - - 参数: - - data (InvoiceApplySchema): 发票申请参数。 - - 返回: - - JSONResponse: 包含发票信息的 JSON 响应。 - """ result = await InvoiceTenantService.apply(auth, data, auth.tenant_id) return SuccessResponse(data=result, msg="发票申请成功") - @TenantInvoiceRouter.get("/list", summary="我的发票列表", response_model=ResponseSchema[PageResultSchema[InvoiceOutSchema]]) async def invoice_list_my_controller( auth: Annotated[AuthSchema, Depends(AuthPermission(["*:*:*"]))], @@ -48,18 +35,6 @@ async def invoice_list_my_controller( page_no: Annotated[int, Query(ge=1)] = 1, page_size: Annotated[int, Query(ge=1, le=100)] = 10, ) -> JSONResponse: - """ - 我的发票列表 - - 参数: - - invoice_type (str | None): 发票类型筛选。 - - status (int | None): 状态筛选。 - - page_no (int): 页码。 - - page_size (int): 每页条数。 - - 返回: - - JSONResponse: 包含分页发票列表的 JSON 响应。 - """ result = await InvoiceTenantService.list_my( auth, auth.tenant_id, @@ -67,35 +42,15 @@ async def invoice_list_my_controller( ) return SuccessResponse(data=result, msg="查询成功") - @TenantInvoiceRouter.get("/{id}/download", summary="下载发票PDF", response_model=ResponseSchema[dict]) async def invoice_download_controller( id: Annotated[int, Path(ge=1)], auth: Annotated[AuthSchema, Depends(AuthPermission(["*:*:*"]))], ) -> JSONResponse: - """ - 下载发票 PDF - - 参数: - - id (int): 发票 ID。 - - 返回: - - JSONResponse: 包含 PDF 下载地址的 JSON 响应。 - """ - crud = InvoiceCRUD(auth) - invoice = await crud.get_or_404(id=id, msg="发票不存在") - if hasattr(invoice, "tenant_id") and invoice.tenant_id != auth.tenant_id: - raise CustomException(msg="发票不存在") - if invoice.status != 1 or not invoice.pdf_url: - raise CustomException(msg="发票未开具或无PDF") - return SuccessResponse(msg="下载地址", data={"pdf_url": invoice.pdf_url}) - - -# ========== 平台端 API ========== - - -PlatformInvoiceRouter = APIRouter(prefix="/invoice", route_class=OperationLogRoute, tags=["发票管理"]) + pdf_url = await InvoiceTenantService.download(auth, id, auth.tenant_id) + return SuccessResponse(msg="下载地址", data={"pdf_url": pdf_url}) +PlatformInvoiceRouter = APIRouter(prefix="/invoice", route_class=OperationLogRoute, tags=["平台管理", "发票管理"]) @PlatformInvoiceRouter.get("/list", summary="全部发票列表", response_model=ResponseSchema[PageResultSchema[InvoiceOutSchema]]) async def invoice_list_all_controller( @@ -106,42 +61,18 @@ async def invoice_list_all_controller( page_no: Annotated[int, Query(ge=1)] = 1, page_size: Annotated[int, Query(ge=1, le=100)] = 10, ) -> JSONResponse: - """ - 全部发票列表(平台端) - - 参数: - - invoice_type (str | None): 发票类型筛选。 - - status (int | None): 状态筛选。 - - tenant_id (int | None): 租户 ID 筛选。 - - page_no (int): 页码。 - - page_size (int): 每页条数。 - - 返回: - - JSONResponse: 包含分页发票列表的 JSON 响应。 - """ result = await InvoicePlatformService.list_all( auth, InvoiceQueryParam(invoice_type=invoice_type, status=status, tenant_id=tenant_id, page_no=page_no, page_size=page_size), ) return SuccessResponse(data=result, msg="查询成功") - @PlatformInvoiceRouter.put("/issue/{id}", summary="开具发票", response_model=ResponseSchema[InvoiceOutSchema]) async def invoice_issue_controller( id: Annotated[int, Path(ge=1)], data: Annotated[InvoiceIssueSchema, Body()], auth: Annotated[AuthSchema, Depends(AuthPermission(["*:*:*"]))], ) -> JSONResponse: - """ - 开具发票 - - 参数: - - id (int): 发票 ID。 - - data (InvoiceIssueSchema): 开票参数(PDF 链接等)。 - - 返回: - - JSONResponse: 包含发票信息的 JSON 响应。 - """ result = await InvoicePlatformService.issue( auth, id, @@ -150,22 +81,11 @@ async def invoice_issue_controller( ) return SuccessResponse(data=result, msg="发票开具成功") - @PlatformInvoiceRouter.put("/void/{id}", summary="作废发票", response_model=ResponseSchema[InvoiceOutSchema]) async def invoice_void_controller( id: Annotated[int, Path(ge=1)], auth: Annotated[AuthSchema, Depends(AuthPermission(["*:*:*"]))], data: Annotated[InvoiceVoidSchema, Body()] = InvoiceVoidSchema(), ) -> JSONResponse: - """ - 作废发票 - - 参数: - - id (int): 发票 ID。 - - data (InvoiceVoidSchema): 作废原因。 - - 返回: - - JSONResponse: 包含发票信息的 JSON 响应。 - """ result = await InvoicePlatformService.void(auth, id, data) return SuccessResponse(data=result, msg="发票已作废") diff --git a/backend/app/api/v1/module_platform/invoice/pdf_helper.py b/backend/app/api/v1/module_platform/invoice/pdf_helper.py index 54c846cd..c473b517 100644 --- a/backend/app/api/v1/module_platform/invoice/pdf_helper.py +++ b/backend/app/api/v1/module_platform/invoice/pdf_helper.py @@ -73,7 +73,7 @@ def _render_invoice_pdf(invoice: InvoiceOutSchema) -> str: output_dir = INVOICE_DIR / str(invoice.tenant_id) output_path = output_dir / f"{invoice.invoice_no}.pdf" generate_pdf_from_template( - template_name="invoice.html.j2", + template_name="invoice.jinja2", template_dir=TEMPLATE_DIR, variables=variables, output_path=output_path, diff --git a/backend/app/api/v1/module_platform/invoice/service.py b/backend/app/api/v1/module_platform/invoice/service.py index f516be38..61ede355 100644 --- a/backend/app/api/v1/module_platform/invoice/service.py +++ b/backend/app/api/v1/module_platform/invoice/service.py @@ -124,6 +124,17 @@ class InvoiceTenantService: ) + @classmethod + async def download(cls, auth: AuthSchema, invoice_id: int, tenant_id: int) -> str: + crud = InvoiceCRUD(auth) + invoice = await crud.get_or_404(id=invoice_id, msg="发票不存在") + if hasattr(invoice, "tenant_id") and invoice.tenant_id != tenant_id: + raise CustomException(msg="发票不存在") + if invoice.status != 1 or not invoice.pdf_url: + raise CustomException(msg="发票未开具或无PDF") + return invoice.pdf_url + + class InvoicePlatformService: """平台端发票服务""" diff --git a/backend/app/api/v1/module_platform/menu/controller.py b/backend/app/api/v1/module_platform/menu/controller.py index b8b3856d..7fe73575 100644 --- a/backend/app/api/v1/module_platform/menu/controller.py +++ b/backend/app/api/v1/module_platform/menu/controller.py @@ -13,7 +13,7 @@ from app.core.router_class import OperationLogRoute from .schema import MenuCreateSchema, MenuOutSchema, MenuQueryParam, MenuUpdateSchema from .service import MenuService -MenuRouter = APIRouter(route_class=OperationLogRoute, prefix="/menu", tags=["菜单管理"]) +MenuRouter = APIRouter(route_class=OperationLogRoute, prefix="/menu", tags=["平台管理", "菜单管理"]) _MENU_NS = "menu" @@ -28,18 +28,9 @@ async def get_menu_tree_controller( search: Annotated[MenuQueryParam, Depends()], auth: Annotated[AuthSchema, Depends(AuthPermission(["module_platform:menu:query"]))], ) -> JSONResponse: - """ - 查询菜单树。 - - 参数: - - search (MenuQueryParam): 查询参数模型。 - - 返回: - - JSONResponse: 包含菜单树的 JSON 响应。 - """ order_by = [{"order": "asc"}] - result_dict_list = await MenuService.tree_service(search=search, auth=auth, order_by=order_by) - return SuccessResponse(data=result_dict_list, msg="查询菜单树成功") + result_dict_tree = await MenuService(auth).tree(search=search, order_by=order_by) + return SuccessResponse(data=result_dict_tree, msg="查询菜单树成功") @MenuRouter.get( @@ -51,16 +42,7 @@ async def get_obj_detail_controller( id: Annotated[int, Path(description="菜单ID")], auth: Annotated[AuthSchema, Depends(AuthPermission(["module_platform:menu:detail"]))], ) -> JSONResponse: - """ - 查询菜单详情。 - - 参数: - - id (int): 菜单ID。 - - 返回: - - JSONResponse: 包含菜单详情的 JSON 响应。 - """ - result_dict = await MenuService.detail_service(id=id, auth=auth) + result_dict = await MenuService(auth).detail(id=id) return SuccessResponse(data=result_dict, msg="查询菜单详情成功") @@ -73,16 +55,7 @@ async def create_obj_controller( data: MenuCreateSchema, auth: Annotated[AuthSchema, Depends(AuthPermission(["module_platform:menu:create"]))], ) -> JSONResponse: - """ - 创建菜单。 - - 参数: - - data (MenuCreateSchema): 菜单创建模型。 - - 返回: - - JSONResponse: 包含创建菜单的 JSON 响应。 - """ - result_dict = await MenuService.create_service(data=data, auth=auth) + result_dict = await MenuService(auth).create(data=data) await FastAPICache.clear(namespace=_MENU_NS) return SuccessResponse(data=result_dict, msg="创建菜单成功") @@ -97,17 +70,7 @@ async def update_obj_controller( id: Annotated[int, Path(description="菜单ID")], auth: Annotated[AuthSchema, Depends(AuthPermission(["module_platform:menu:update"]))], ) -> JSONResponse: - """ - 修改菜单。 - - 参数: - - id (int): 菜单ID。 - - data (MenuUpdateSchema): 菜单更新模型。 - - 返回: - - JSONResponse: 包含修改菜单的 JSON 响应。 - """ - result_dict = await MenuService.update_service(id=id, data=data, auth=auth) + result_dict = await MenuService(auth).update(id=id, data=data) await FastAPICache.clear(namespace=_MENU_NS) return SuccessResponse(data=result_dict, msg="修改菜单成功") @@ -121,16 +84,7 @@ async def delete_obj_controller( ids: Annotated[list[int], Body(description="ID列表")], auth: Annotated[AuthSchema, Depends(AuthPermission(["module_platform:menu:delete"]))], ) -> JSONResponse: - """ - 删除菜单。 - - 参数: - - ids (list[int]): 菜单ID列表。 - - 返回: - - JSONResponse: 包含删除菜单的 JSON 响应。 - """ - await MenuService.delete_service(ids=ids, auth=auth) + await MenuService(auth).delete(ids=ids) await FastAPICache.clear(namespace=_MENU_NS) return SuccessResponse(msg="删除菜单成功") @@ -144,15 +98,6 @@ async def batch_set_available_obj_controller( data: BatchSetAvailable, auth: Annotated[AuthSchema, Depends(AuthPermission(["module_platform:menu:patch"]))], ) -> JSONResponse: - """ - 批量修改菜单状态。 - - 参数: - - data (BatchSetAvailable): 批量修改菜单状态模型。 - - 返回: - - JSONResponse: 批量修改菜单状态的 JSON 响应。 - """ - await MenuService.set_available_service(data=data, auth=auth) + await MenuService(auth).set_available(data=data) await FastAPICache.clear(namespace=_MENU_NS) return SuccessResponse(msg="批量修改菜单状态成功") diff --git a/backend/app/api/v1/module_platform/menu/model.py b/backend/app/api/v1/module_platform/menu/model.py index 40127903..4756b71a 100644 --- a/backend/app/api/v1/module_platform/menu/model.py +++ b/backend/app/api/v1/module_platform/menu/model.py @@ -25,7 +25,7 @@ class MenuModel(ModelMixin): __table_args__: dict[str, str] = {"comment": "平台菜单表"} __tree_children_attr__: str = "children" __loader_options__: list[str] = ["roles", "children"] - __permission_strategy__: PermissionFilterStrategy = PermissionFilterStrategy.ROLE_BASED + __permission_strategy__: PermissionFilterStrategy = PermissionFilterStrategy.MENU_AUTH name: Mapped[str] = mapped_column(String(50), nullable=False, comment="菜单名称") type: Mapped[int] = mapped_column(Integer, nullable=False, default=2, comment="菜单类型(1:目录 2:菜单 3:按钮 4:链接)") diff --git a/backend/app/api/v1/module_platform/menu/service.py b/backend/app/api/v1/module_platform/menu/service.py index 39795b4b..d90be7a0 100644 --- a/backend/app/api/v1/module_platform/menu/service.py +++ b/backend/app/api/v1/module_platform/menu/service.py @@ -22,23 +22,19 @@ from .schema import ( class MenuService: - """ - 菜单管理服务(查询操作租户可见,写操作仅超级管理员可操作) - """ + """菜单管理服务(查询操作租户可见,写操作仅超级管理员可操作)""" - @classmethod - async def _validate_parent_child_type(cls, auth: AuthSchema, parent_id: int | None, child_type: int) -> None: - """ - 父子类型约束:目录下仅允许目录/菜单/外链;菜单下仅允许按钮;按钮与外链下不可挂子级。 - 无父级时仅允许目录、菜单、外链(与前端一致)。 - """ + def __init__(self, auth: AuthSchema) -> None: + self.auth = auth + + async def _validate_parent_child_type(self, parent_id: int | None, child_type: int) -> None: if parent_id is None: if child_type is None: return if child_type not in (1, 2, 4): raise CustomException(msg="顶级菜单仅允许目录、菜单或外链类型") return - parent = await MenuCRUD(auth).get(id=parent_id) + parent = await MenuCRUD(self.auth).get(id=parent_id) if not parent: raise CustomException(msg="父级菜单不存在") pt = parent.type @@ -51,198 +47,113 @@ class MenuService: else: raise CustomException(msg="菜单或链接类型下不允许新增子菜单") - @classmethod - async def _validate_parent_child_client(cls, auth: AuthSchema, parent_id: int | None, client: str) -> None: - """子菜单终端须与父菜单一致。""" + async def _validate_parent_child_client(self, parent_id: int | None, client: str) -> None: if parent_id is None or client is None: return - parent = await MenuCRUD(auth).get(id=parent_id) + parent = await MenuCRUD(self.auth).get(id=parent_id) if not parent: return p_client = getattr(parent, "client", None) or "pc" if p_client != client: raise CustomException(msg="子菜单终端须与父菜单一致(均为 pc 或均为 app)") - @classmethod - async def detail_service(cls, auth: AuthSchema, id: int) -> MenuOutSchema: - """ - 获取菜单详情。 - - 参数: - - auth (AuthSchema): 认证对象。 - - id (int): 菜单ID。 - - 返回: - - MenuOutSchema: 菜单详情对象。 - """ - menu = await MenuCRUD(auth).get(id=id, preload=["roles"]) + async def detail(self, id: int) -> MenuOutSchema: + menu = await MenuCRUD(self.auth).get(id=id, preload=["roles"]) if not menu: raise CustomException(msg="菜单不存在") menu_out = MenuOutSchema.model_validate(menu) if menu.parent_id: - parent = await MenuCRUD(auth).get(id=menu.parent_id) + parent = await MenuCRUD(self.auth).get(id=menu.parent_id) if parent: menu_out.parent_name = parent.name return menu_out - @classmethod - async def tree_service( - cls, - auth: AuthSchema, + async def tree( + self, search: MenuQueryParam | None = None, order_by: list[dict] | None = None, ) -> list[dict]: - """ - 获取菜单树形列表。 - - 参数: - - auth (AuthSchema): 认证对象。 - - search (MenuQueryParam | None): 查询参数对象。 - - order_by (list[dict] | None): 排序参数列表。 - - 返回: - - list[dict]: 菜单树形列表对象。 - """ - # 使用树形结构查询,预加载children关系 - menu_list = await MenuCRUD(auth).tree_list(search=vars(search) if search else None, order_by=order_by) - # 转换为字典列表(使用树形 Schema) + menu_list = await MenuCRUD(self.auth).tree_list(search=vars(search) if search else None, order_by=order_by) menu_dict_list = [MenuTreeOutSchema.model_validate(menu).model_dump() for menu in menu_list] - # 使用traversal_to_tree构建树形结构 return traversal_to_tree(menu_dict_list) - @classmethod @require_superadmin - async def create_service(cls, auth: AuthSchema, data: MenuCreateSchema) -> MenuOutSchema: - """ - 创建菜单。 - - 参数: - - auth (AuthSchema): 认证对象。 - - data (MenuCreateSchema): 创建参数对象。 - - 返回: - - MenuOutSchema: 创建的菜单对象。 - """ + async def create(self, data: MenuCreateSchema) -> MenuOutSchema: search: dict[str, Any] = {} if data.title is not None: search["title"] = data.title if data.parent_id is not None: search["parent_id"] = data.parent_id - menu = await MenuCRUD(auth).get(**search) + menu = await MenuCRUD(self.auth).get(**search) if menu: raise CustomException(msg="创建失败,该菜单已存在") - await cls._validate_parent_child_type(auth, data.parent_id, data.type) - await cls._validate_parent_child_client(auth, data.parent_id, data.client) + await self._validate_parent_child_type(data.parent_id, data.type) + await self._validate_parent_child_client(data.parent_id, data.client) - new_menu = await MenuCRUD(auth).create(data=data) + new_menu = await MenuCRUD(self.auth).create(data=data) return MenuOutSchema.model_validate(new_menu) - @classmethod @require_superadmin - async def update_service(cls, auth: AuthSchema, id: int, data: MenuUpdateSchema) -> MenuOutSchema: - """ - 更新菜单。 - - 参数: - - auth (AuthSchema): 认证对象。 - - id (int): 菜单ID。 - - data (MenuUpdateSchema): 更新参数对象。 - - 返回: - - MenuOutSchema: 更新的菜单对象。 - """ - _ = await MenuCRUD(auth).get_or_404(id=id, msg="更新失败,该菜单不存在") - await cls._validate_parent_child_type(auth, data.parent_id, data.type) - await cls._validate_parent_child_client(auth, data.parent_id, data.client) + async def update(self, id: int, data: MenuUpdateSchema) -> MenuOutSchema: + _ = await MenuCRUD(self.auth).get_or_404(id=id, msg="更新失败,该菜单不存在") + await self._validate_parent_child_type(data.parent_id, data.type) + await self._validate_parent_child_client(data.parent_id, data.client) if data.title is not None: search: dict[str, Any] = {"title": data.title} if data.parent_id is not None: search["parent_id"] = data.parent_id - exist_menu = await MenuCRUD(auth).get(**search) + exist_menu = await MenuCRUD(self.auth).get(**search) if exist_menu and exist_menu.id != id: raise CustomException(msg="更新失败,菜单标题重复") if data.parent_id: - parent_menu = await MenuCRUD(auth).get(id=data.parent_id) + parent_menu = await MenuCRUD(self.auth).get(id=data.parent_id) if not parent_menu: raise CustomException(msg="更新失败,父级菜单不存在") - new_menu = await MenuCRUD(auth).update(id=id, data=data) + new_menu = await MenuCRUD(self.auth).update(id=id, data=data) if data.status is not None: - await cls.set_available_service(auth=auth, data=BatchSetAvailable(ids=[id], status=data.status)) + await self.set_available(data=BatchSetAvailable(ids=[id], status=data.status)) menu_out = MenuOutSchema.model_validate(new_menu) if menu_out.parent_id: - parent = await MenuCRUD(auth).get(id=menu_out.parent_id) + parent = await MenuCRUD(self.auth).get(id=menu_out.parent_id) if parent: menu_out.parent_name = parent.name return menu_out - @classmethod @require_superadmin - async def delete_service(cls, auth: AuthSchema, ids: list[int]) -> None: - """ - 删除菜单。 - - 参数: - - auth (AuthSchema): 认证对象。 - - ids (list[int]): 菜单ID列表。 - - 返回: - - None - """ + async def delete(self, ids: list[int]) -> None: if len(ids) < 1: raise CustomException(msg="删除失败,删除对象不能为空") - # 获取所有菜单列表,用于构建树形关系 - all_menus = await MenuCRUD(auth).list() - - # 构建子菜单ID映射 + all_menus = await MenuCRUD(self.auth).list() child_id_map = get_child_id_map(model_list=all_menus) - # 收集所有需要删除的菜单ID,包括直接指定的ID和它们的所有子菜单ID delete_ids_set = set() - - for id in ids: - # 递归获取该ID的所有子菜单ID - all_descendants = get_child_recursion(id=id, id_map=child_id_map) + for mid in ids: + all_descendants = get_child_recursion(id=mid, id_map=child_id_map) delete_ids_set.update(all_descendants) - # 将集合转换为列表 delete_ids = list(delete_ids_set) + await MenuCRUD(self.auth).delete(ids=delete_ids) - # 执行批量删除操作 - await MenuCRUD(auth).delete(ids=delete_ids) - - @classmethod @require_superadmin - async def set_available_service(cls, auth: AuthSchema, data: BatchSetAvailable) -> None: - """ - 递归获取所有父、子级菜单,然后批量修改菜单可用状态。 - - 参数: - - auth (AuthSchema): 认证对象。 - - data (BatchSetAvailable): 批量设置可用参数对象。 - - 返回: - - None - """ - menu_list = await MenuCRUD(auth).list() + async def set_available(self, data: BatchSetAvailable) -> None: + menu_list = await MenuCRUD(self.auth).list() total_ids = [] if data.status == 0: - # 激活,则需要把所有父级菜单都激活 id_map = get_parent_id_map(model_list=menu_list) for menu_id in data.ids: enable_ids = get_parent_recursion(id=menu_id, id_map=id_map) total_ids.extend(enable_ids) else: - # 禁止,则需要把所有子级菜单都禁止 id_map = get_child_id_map(model_list=menu_list) for menu_id in data.ids: disable_ids = get_child_recursion(id=menu_id, id_map=id_map) total_ids.extend(disable_ids) - await MenuCRUD(auth).set(ids=total_ids, status=data.status) + await MenuCRUD(self.auth).set(ids=total_ids, status=data.status) diff --git a/backend/app/api/v1/module_platform/order/controller.py b/backend/app/api/v1/module_platform/order/controller.py index fc116b6a..293dff06 100644 --- a/backend/app/api/v1/module_platform/order/controller.py +++ b/backend/app/api/v1/module_platform/order/controller.py @@ -28,16 +28,10 @@ from .schema import ( ) from .service import OrderService, PaymentService, RefundService -OrderRouter = APIRouter(route_class=OperationLogRoute, prefix="/order", tags=["订单管理"]) -PaymentRouter = APIRouter(route_class=OperationLogRoute, prefix="/payment", tags=["支付管理"]) -RefundRouter = APIRouter(route_class=OperationLogRoute, prefix="/refund", tags=["退款管理"]) -TenantOrderRouter = APIRouter(route_class=OperationLogRoute, prefix="/order", tags=["租户订单"]) - - -def _make_bare_auth(db: AsyncSession) -> AuthSchema: - """构造无用户上下文的 AuthSchema(支付回调等场景)""" - return AuthSchema(db=db, check_data_scope=False) - +OrderRouter = APIRouter(route_class=OperationLogRoute, prefix="/order", tags=["平台管理", "订单管理"]) +PaymentRouter = APIRouter(route_class=OperationLogRoute, prefix="/payment", tags=["平台管理", "支付管理"]) +RefundRouter = APIRouter(route_class=OperationLogRoute, prefix="/refund", tags=["平台管理", "退款管理"]) +TenantOrderRouter = APIRouter(route_class=OperationLogRoute, prefix="/order", tags=["平台管理", "租户订单"]) @OrderRouter.post( "/create", @@ -48,20 +42,9 @@ async def order_create_controller( data: Annotated[OrderCreateSchema, Body()], auth: Annotated[AuthSchema, Depends(AuthPermission(["module_platform:order:create"]))], ) -> JSONResponse: - """ - 创建订单 - - 参数: - - data (OrderCreateSchema): 订单创建参数。 - - auth (AuthSchema): 认证信息模型。 - - 返回: - - JSONResponse: 包含订单详情的 JSON 响应。 - """ result = await OrderService.create_order(auth=auth, data=data) return SuccessResponse(data=result, msg="订单创建成功") - @OrderRouter.get( "/detail/{order_id}", summary="订单详情", @@ -71,22 +54,11 @@ async def order_detail_controller( order_id: Annotated[int, Path(ge=1)], auth: Annotated[AuthSchema, Depends(AuthPermission(["module_platform:order:query"]))], ) -> JSONResponse: - """ - 订单详情 - - 参数: - - order_id (int): 订单 ID。 - - auth (AuthSchema): 认证信息模型。 - - 返回: - - JSONResponse: 包含订单详情的 JSON 响应。 - """ order = await OrderService.get_detail(auth=auth, order_id=order_id) if not order: raise HTTPException(status_code=404, detail="订单不存在") return SuccessResponse(data=order) - @OrderRouter.get( "/list", summary="订单列表", @@ -100,20 +72,6 @@ async def order_list_controller( page: Annotated[int, Query(ge=1)] = 1, page_size: Annotated[int, Query(ge=1, le=100)] = 20, ) -> JSONResponse: - """ - 订单列表 - - 参数: - - auth (AuthSchema): 认证信息模型。 - - tenant_id (int | None): 租户 ID 筛选。 - - status (int | None): 状态筛选。 - - order_type (str | None): 订单类型筛选。 - - page (int): 页码。 - - page_size (int): 每页条数。 - - 返回: - - JSONResponse: 包含分页订单列表的 JSON 响应。 - """ params = OrderQueryParam(tenant_id=tenant_id, status=status, order_type=order_type) offset = (page - 1) * page_size items, total = await OrderService.get_list(auth=auth, params=params, offset=offset, limit=page_size) @@ -126,7 +84,6 @@ async def order_list_controller( ) return SuccessResponse(data=result) - @OrderRouter.post( "/cancel/{order_id}", summary="取消订单", @@ -136,20 +93,9 @@ async def order_cancel_controller( order_id: Annotated[int, Path(ge=1)], auth: Annotated[AuthSchema, Depends(AuthPermission(["module_platform:order:update"]))], ) -> JSONResponse: - """ - 取消订单 - - 参数: - - order_id (int): 订单 ID。 - - auth (AuthSchema): 认证信息模型。 - - 返回: - - JSONResponse: 包含取消结果的 JSON 响应。 - """ result = await OrderService.cancel_order(auth=auth, order_id=order_id) return SuccessResponse(data=result, msg=result["message"]) - @PaymentRouter.post( "/pay/{order_id}", summary="创建支付(获取支付 URL/二维码)", @@ -161,25 +107,10 @@ async def payment_create_controller( request: Request, method: Annotated[str, Query(description="支付渠道: alipay / wxpay(留空=自动)")] = "", ) -> JSONResponse: - """ - 创建支付 - - 调用支付网关生成支付 URL(H5 跳转)或二维码(Native 扫码)。 - - 参数: - - order_id (int): 订单 ID。 - - auth (AuthSchema): 认证信息模型。 - - request (Request): FastAPI 请求对象。 - - method (str): 支付渠道。 - - 返回: - - JSONResponse: 包含支付信息的 JSON 响应。 - """ base_url = str(request.base_url).rstrip("/") result = await PaymentService.create_payment(auth=auth, order_id=order_id, method=method, notify_base_url=base_url) return SuccessResponse(data=result, msg="支付信息已生成") - @PaymentRouter.get( "/status/{order_id}", summary="查询支付状态(供前端轮询)", @@ -189,21 +120,10 @@ async def payment_status_controller( order_id: Annotated[int, Path(ge=1)], db: Annotated[AsyncSession, Depends(db_getter)], ) -> JSONResponse: - """ - 查询支付状态(无需登录,前端轮询用) - - 参数: - - order_id (int): 订单 ID。 - - db (AsyncSession): 数据库会话。 - - 返回: - - JSONResponse: 包含支付状态的 JSON 响应。 - """ - auth = _make_bare_auth(db) + auth = AuthSchema(db=db, check_data_scope=False) result = await OrderService.check_payment_status(auth=auth, order_id=order_id) return SuccessResponse(data=result) - @PaymentRouter.post( "/callback/{method}", summary="支付回调(统一入口)", @@ -214,19 +134,8 @@ async def payment_callback_controller( data: Annotated[dict, Body()], db: Annotated[AsyncSession, Depends(db_getter)], ) -> JSONResponse: - """ - 接收支付网关的异步通知(无需认证) - - 参数: - - method (str): 支付渠道。 - - data (dict): 支付回调原始数据。 - - db (AsyncSession): 数据库会话。 - - 返回: - - JSONResponse: 包含处理结果的 JSON 响应。 - """ try: - auth = _make_bare_auth(db) + auth = AuthSchema(db=db, check_data_scope=False) result = await PaymentService.handle_callback(auth=auth, method=method, callback_data=data) logger.info(f"支付回调处理成功: {result}") return SuccessResponse(data=result) @@ -234,7 +143,6 @@ async def payment_callback_controller( logger.warning(f"支付回调处理失败: {e}") return SuccessResponse(data={"message": str(e)}, code=400) - @PaymentRouter.post( "/mock/callback", summary="Mock 支付回调(开发环境触发模拟支付)", @@ -244,21 +152,11 @@ async def payment_mock_callback_controller( order_id: Annotated[int, Body(ge=1, description="订单 ID")], db: Annotated[AsyncSession, Depends(db_getter)], ) -> JSONResponse: - """ - 开发环境下手动触发模拟支付成功回调 - - 参数: - - order_id (int): 订单 ID。 - - db (AsyncSession): 数据库会话。 - - 返回: - - JSONResponse: 包含模拟支付结果的 JSON 响应。 - """ - from app.core.payment import get_mock_gateway + from app.utils.payment import get_mock_gateway from .crud import OrderCRUD - auth = _make_bare_auth(db) + auth = AuthSchema(db=db, check_data_scope=False) order = await OrderCRUD(auth).get_by_id(order_id) if not order: raise HTTPException(status_code=404, detail="订单不存在") @@ -269,7 +167,6 @@ async def payment_mock_callback_controller( logger.info(f"Mock 支付回调触发: order_id={order_id}") return SuccessResponse(data=result, msg="模拟支付成功") - @PaymentRouter.get( "/record/list", summary="支付记录列表", @@ -280,17 +177,6 @@ async def payment_record_list_controller( page: Annotated[int, Query(ge=1)] = 1, page_size: Annotated[int, Query(ge=1, le=100)] = 20, ) -> JSONResponse: - """ - 支付记录列表 - - 参数: - - auth (AuthSchema): 认证信息模型。 - - page (int): 页码。 - - page_size (int): 每页条数。 - - 返回: - - JSONResponse: 包含分页支付记录列表的 JSON 响应。 - """ offset = (page - 1) * page_size items, total = await PaymentService.get_records(auth=auth, offset=offset, limit=page_size) result = PageResultSchema( @@ -302,7 +188,6 @@ async def payment_record_list_controller( ) return SuccessResponse(data=result) - @RefundRouter.get( "/list", summary="退款审核列表", @@ -314,18 +199,6 @@ async def refund_list_controller( page: Annotated[int, Query(ge=1)] = 1, page_size: Annotated[int, Query(ge=1, le=100)] = 20, ) -> JSONResponse: - """ - 退款审核列表 - - 参数: - - auth (AuthSchema): 认证信息模型。 - - status (int | None): 状态筛选。 - - page (int): 页码。 - - page_size (int): 每页条数。 - - 返回: - - JSONResponse: 包含分页退款列表的 JSON 响应。 - """ offset = (page - 1) * page_size items, total = await RefundService.get_list(auth=auth, status=status, offset=offset, limit=page_size) result = PageResultSchema( @@ -337,7 +210,6 @@ async def refund_list_controller( ) return SuccessResponse(data=result) - @RefundRouter.put( "/approve/{refund_id}", summary="批准退款", @@ -347,16 +219,6 @@ async def refund_approve_controller( refund_id: Annotated[int, Path(ge=1)], auth: Annotated[AuthSchema, Depends(AuthPermission(["module_platform:order:update"]))], ) -> JSONResponse: - """ - 批准退款 - - 参数: - - refund_id (int): 退款申请 ID。 - - auth (AuthSchema): 认证信息模型。 - - 返回: - - JSONResponse: 包含批准结果的 JSON 响应。 - """ result = await RefundService.approve( auth=auth, refund_id=refund_id, @@ -365,7 +227,6 @@ async def refund_approve_controller( ) return SuccessResponse(data=result, msg=result["message"]) - @RefundRouter.put( "/reject/{refund_id}", summary="驳回退款", @@ -376,17 +237,6 @@ async def refund_reject_controller( data: Annotated[RefundReviewSchema, Body()], auth: Annotated[AuthSchema, Depends(AuthPermission(["module_platform:order:update"]))], ) -> JSONResponse: - """ - 驳回退款 - - 参数: - - refund_id (int): 退款申请 ID。 - - data (RefundReviewSchema): 驳回原因。 - - auth (AuthSchema): 认证信息模型。 - - 返回: - - JSONResponse: 包含驳回结果的 JSON 响应。 - """ result = await RefundService.reject( auth=auth, refund_id=refund_id, @@ -396,7 +246,6 @@ async def refund_reject_controller( ) return SuccessResponse(data=result, msg=result["message"]) - @TenantOrderRouter.post( "/create", summary="租户端创建订单", @@ -406,22 +255,11 @@ async def tenant_order_create_controller( data: Annotated[OrderCreateSchema, Body()], auth: Annotated[AuthSchema, Depends(AuthPermission(["tenant:order:create"]))], ) -> JSONResponse: - """ - 租户端创建订单 - - 参数: - - data (OrderCreateSchema): 订单创建参数。 - - auth (AuthSchema): 认证信息模型。 - - 返回: - - JSONResponse: 包含订单详情的 JSON 响应。 - """ if data.tenant_id != auth.tenant_id: raise HTTPException(status_code=403, detail="无权操作") result = await OrderService.create_order(auth=auth, data=data) return SuccessResponse(data=result, msg="订单创建成功") - @TenantOrderRouter.post( "/refund/apply/{order_id}", summary="申请退款", @@ -432,16 +270,5 @@ async def tenant_refund_apply_controller( data: Annotated[RefundApplySchema, Body()], auth: Annotated[AuthSchema, Depends(AuthPermission(["tenant:order:refund"]))], ) -> JSONResponse: - """ - 租户端申请退款 - - 参数: - - order_id (int): 订单 ID。 - - data (RefundApplySchema): 退款申请参数。 - - auth (AuthSchema): 认证信息模型。 - - 返回: - - JSONResponse: 包含退款申请结果的 JSON 响应。 - """ result = await RefundService.apply(auth=auth, data=data, order_id=order_id) return SuccessResponse(data=result, msg="退款申请已提交") diff --git a/backend/app/api/v1/module_platform/order/crud.py b/backend/app/api/v1/module_platform/order/crud.py index 29bed975..b7710b11 100644 --- a/backend/app/api/v1/module_platform/order/crud.py +++ b/backend/app/api/v1/module_platform/order/crud.py @@ -1,6 +1,5 @@ """订单与支付 CRUD""" -from datetime import datetime from typing import Any from app.core.base_crud import CRUDBase @@ -52,20 +51,6 @@ class OrderCRUD(CRUDBase[OrderModel, OrderCreateInternalSchema, OrderUpdateInter .values(status=3) ) - @staticmethod - async def cancel_expired_orders() -> None: - """定时任务:取消超时未支付订单""" - from sqlalchemy import update as sa_update - - from app.core.database import async_db_session - from app.core.logger import logger - - now = datetime.now() - async with async_db_session() as session: - async with session.begin(): - result = await session.execute(sa_update(OrderModel).where(OrderModel.status == 0).where(OrderModel.expire_time < now).where(OrderModel.is_deleted.is_(False)).values(status=2)) - logger.info(f"超时订单取消: 已取消 {result.rowcount} 条订单") - class PaymentRecordCRUD(CRUDBase[PaymentRecordModel, PaymentRecordCreateSchema, Any]): """支付记录 CRUD""" diff --git a/backend/app/api/v1/module_platform/order/service.py b/backend/app/api/v1/module_platform/order/service.py index 8d370427..3dcf5a3b 100644 --- a/backend/app/api/v1/module_platform/order/service.py +++ b/backend/app/api/v1/module_platform/order/service.py @@ -8,7 +8,7 @@ from sqlalchemy import select from app.core.base_schema import AuthSchema from app.core.exceptions import CustomException from app.core.logger import logger -from app.core.payment import create_payment_gateway +from app.utils.payment import create_payment_gateway from .crud import OrderCRUD, PaymentRecordCRUD, RefundCRUD from .model import OrderModel @@ -186,15 +186,23 @@ class OrderService: pay_time=order.pay_time.isoformat() if order.pay_time else None, ) - @classmethod - async def cancel_expired_orders(cls) -> None: - """ - 定时任务:取消超时未支付的订单 + @staticmethod + async def cancel_expired_orders() -> None: + from sqlalchemy import update as sa_update - 返回: - - None - """ - await OrderCRUD.cancel_expired_orders() + from app.core.database import async_db_session + + now = datetime.now() + async with async_db_session() as session: + async with session.begin(): + result = await session.execute( + sa_update(OrderModel) + .where(OrderModel.status == 0) + .where(OrderModel.expire_time < now) + .where(OrderModel.is_deleted == False) # noqa: E712 + .values(status=2) + ) + logger.info(f"超时订单取消: 已取消 {result.rowcount} 条订单") class PaymentService: @@ -373,7 +381,7 @@ class PaymentService: await auth.db.flush() if tenant.contact_email: - await PaymentService._send_order_email(order, pkg, tenant) + await PaymentService._send_order_email(auth, order, pkg, tenant) @classmethod async def _activate_plugin(cls, auth: AuthSchema, order: OrderModel) -> None: @@ -422,7 +430,7 @@ class PaymentService: tenant = await auth.db.get(TenantModel, order.tenant_id) if tenant and tenant.contact_email: - await PaymentService._send_order_email(order, plugin, tenant, order_type_label="购买") + await PaymentService._send_order_email(auth, order, plugin, tenant, order_type_label="购买") @classmethod async def _check_downgrade_quota(cls, auth: AuthSchema, tenant_id: int, new_pkg: object) -> None: @@ -466,7 +474,7 @@ class PaymentService: raise CustomException(msg=f"降级失败:当前租户已有 {current} 个{label},超过目标套餐限额 {limit}") @classmethod - async def _send_order_email(cls, order: "OrderModel", product: object, tenant: object, order_type_label: str = "") -> None: + async def _send_order_email(cls, auth: AuthSchema, order: "OrderModel", product: object, tenant: object, order_type_label: str = "") -> None: """ 发送购买确认邮件(失败静默降级) @@ -495,7 +503,7 @@ class PaymentService: product_name = getattr(product, "name", "") if product else "" amount_str = f"{order.amount / 100:.2f}" if order.amount else "0.00" - await EmailSendService.send_by_template( + await EmailSendService(auth).send_by_template( to_email=tenant.contact_email, to_name=tenant.contact_name or tenant.name, template_code="order_confirmation", diff --git a/backend/app/api/v1/module_platform/package/controller.py b/backend/app/api/v1/module_platform/package/controller.py index 995ed938..7d4d7103 100644 --- a/backend/app/api/v1/module_platform/package/controller.py +++ b/backend/app/api/v1/module_platform/package/controller.py @@ -21,11 +21,10 @@ from .schema import ( ) from .service import PackageService -PackageRouter = APIRouter(route_class=OperationLogRoute, prefix="/package", tags=["套餐管理"]) +PackageRouter = APIRouter(route_class=OperationLogRoute, prefix="/package", tags=["平台管理", "套餐管理"]) _PKG_NS = "package" - @PackageRouter.get( "/detail/{id}", summary="获取套餐详情", @@ -36,19 +35,9 @@ async def get_obj_detail_controller( id: Annotated[int, Path(description="套餐ID")], auth: Annotated[AuthSchema, Depends(AuthPermission(["module_package:package:query"]))], ) -> JSONResponse: - """ - 获取套餐详情 - - 参数: - - id (int): 套餐 ID。 - - 返回: - - JSONResponse: 包含套餐详情的 JSON 响应。 - """ - result_dict = await PackageService.detail_service(auth=auth, id=id) + result_dict = await PackageService(auth).detail(id=id) return SuccessResponse(data=result_dict, msg="获取套餐详情成功") - @PackageRouter.get( "/list", summary="获取套餐列表", @@ -59,18 +48,7 @@ async def get_obj_list_controller( search: Annotated[PackageQueryParam, Depends()], auth: Annotated[AuthSchema, Depends(AuthPermission(["module_package:package:query"]))], ) -> JSONResponse: - """ - 获取套餐列表 - - 参数: - - page (PaginationQueryParam): 分页查询参数。 - - search (PackageQueryParam): 查询筛选参数。 - - 返回: - - JSONResponse: 包含分页套餐列表的 JSON 响应。 - """ - result_dict = await PackageService.page_service( - auth=auth, + result_dict = await PackageService(auth).page( page_no=page.page_no, page_size=page.page_size, search=search, @@ -78,7 +56,6 @@ async def get_obj_list_controller( ) return SuccessResponse(data=result_dict, msg="查询成功") - @PackageRouter.post( "/create", summary="创建套餐", @@ -88,20 +65,10 @@ async def create_obj_controller( data: Annotated[PackageCreateSchema, Body(description="套餐信息")], auth: Annotated[AuthSchema, Depends(AuthPermission(["module_package:package:create"]))], ) -> JSONResponse: - """ - 创建套餐 - - 参数: - - data (PackageCreateSchema): 套餐创建参数。 - - 返回: - - JSONResponse: 包含创建后的套餐详情的 JSON 响应。 - """ - result_dict = await PackageService.create_service(auth=auth, data=data) + result_dict = await PackageService(auth).create(data=data) await FastAPICache.clear(namespace=_PKG_NS) return SuccessResponse(data=result_dict, msg="创建成功") - @PackageRouter.put( "/update/{id}", summary="更新套餐", @@ -112,21 +79,10 @@ async def update_obj_controller( data: Annotated[PackageUpdateSchema, Body(description="套餐信息")], auth: Annotated[AuthSchema, Depends(AuthPermission(["module_package:package:update"]))], ) -> JSONResponse: - """ - 更新套餐 - - 参数: - - id (int): 套餐 ID。 - - data (PackageUpdateSchema): 套餐更新参数。 - - 返回: - - JSONResponse: 包含更新后的套餐详情的 JSON 响应。 - """ - result_dict = await PackageService.update_service(auth=auth, id=id, data=data) + result_dict = await PackageService(auth).update(id=id, data=data) await FastAPICache.clear(namespace=_PKG_NS) return SuccessResponse(data=result_dict, msg="更新成功") - @PackageRouter.delete( "/delete", summary="删除套餐", @@ -136,20 +92,10 @@ async def delete_obj_controller( ids: Annotated[list[int], Body(description="ID列表")], auth: Annotated[AuthSchema, Depends(AuthPermission(["module_package:package:delete"]))], ) -> JSONResponse: - """ - 删除套餐 - - 参数: - - ids (list[int]): 套餐 ID 列表。 - - 返回: - - JSONResponse: 删除结果。 - """ - await PackageService.delete_service(auth=auth, ids=ids) + await PackageService(auth).delete(ids=ids) await FastAPICache.clear(namespace=_PKG_NS) return SuccessResponse(msg="删除成功") - @PackageRouter.patch( "/status/batch", summary="批量修改状态", @@ -159,21 +105,11 @@ async def set_available_controller( data: Annotated[BatchSetAvailable, Body(description="状态设置")], auth: Annotated[AuthSchema, Depends(AuthPermission(["module_package:package:update"]))], ) -> JSONResponse: - """ - 批量修改套餐状态 - - 参数: - - data (BatchSetAvailable): 批量状态设置参数。 - - 返回: - - JSONResponse: 操作结果。 - """ for id in data.ids: - await PackageService.update_service(auth=auth, id=id, data=PackageUpdateSchema(status=data.status)) + await PackageService(auth).update(id=id, data=PackageUpdateSchema(status=data.status)) await FastAPICache.clear(namespace=_PKG_NS) return SuccessResponse(msg="状态设置成功") - @PackageRouter.get( "/menus/{package_id}", summary="获取套餐菜单", @@ -183,19 +119,9 @@ async def get_menus_controller( package_id: Annotated[int, Path(description="套餐ID")], auth: Annotated[AuthSchema, Depends(AuthPermission(["module_package:package:query"]))], ) -> JSONResponse: - """ - 获取套餐关联菜单 - - 参数: - - package_id (int): 套餐 ID。 - - 返回: - - JSONResponse: 包含菜单 ID 列表的 JSON 响应。 - """ - result = await PackageService.get_menus_service(auth=auth, package_id=package_id) + result = await PackageService(auth).get_menus(package_id=package_id) return SuccessResponse(data=result, msg="获取成功") - @PackageRouter.post( "/menus/{package_id}/set", summary="设置套餐菜单", @@ -206,20 +132,9 @@ async def set_menus_controller( data: Annotated[PackageMenuSetSchema, Body(description="菜单列表")], auth: Annotated[AuthSchema, Depends(AuthPermission(["module_package:package:update"]))], ) -> JSONResponse: - """ - 设置套餐菜单权限 - - 参数: - - package_id (int): 套餐 ID。 - - data (PackageMenuSetSchema): 菜单 ID 列表。 - - 返回: - - JSONResponse: 操作结果。 - """ - await PackageService.set_menus_service(auth=auth, package_id=package_id, data=data) + await PackageService(auth).set_menus(package_id=package_id, data=data) return SuccessResponse(msg="设置成功") - @PackageRouter.get( "/plugins/{package_id}", summary="获取套餐插件", @@ -229,19 +144,9 @@ async def get_plugins_controller( package_id: Annotated[int, Path(description="套餐ID")], auth: Annotated[AuthSchema, Depends(AuthPermission(["module_package:package:query"]))], ) -> JSONResponse: - """ - 获取套餐关联插件 - - 参数: - - package_id (int): 套餐 ID。 - - 返回: - - JSONResponse: 包含插件 ID 列表的 JSON 响应。 - """ - result = await PackageService.get_plugins_service(auth=auth, package_id=package_id) + result = await PackageService(auth).get_plugins(package_id=package_id) return SuccessResponse(data=result, msg="获取成功") - @PackageRouter.post( "/plugins/{package_id}/set", summary="设置套餐插件", @@ -252,15 +157,5 @@ async def set_plugins_controller( data: Annotated[PackagePluginSetSchema, Body(description="插件列表")], auth: Annotated[AuthSchema, Depends(AuthPermission(["module_package:package:update"]))], ) -> JSONResponse: - """ - 设置套餐插件 - - 参数: - - package_id (int): 套餐 ID。 - - data (PackagePluginSetSchema): 插件 ID 列表。 - - 返回: - - JSONResponse: 操作结果。 - """ - await PackageService.set_plugins_service(auth=auth, package_id=package_id, data=data) + await PackageService(auth).set_plugins(package_id=package_id, data=data) return SuccessResponse(msg="设置成功") diff --git a/backend/app/api/v1/module_platform/package/service.py b/backend/app/api/v1/module_platform/package/service.py index 90df4c87..b4c387d4 100644 --- a/backend/app/api/v1/module_platform/package/service.py +++ b/backend/app/api/v1/module_platform/package/service.py @@ -20,49 +20,24 @@ from .schema import ( class PackageService: - """ - 套餐管理服务(仅超级管理员可操作) - """ + """套餐管理服务(仅超级管理员可操作)""" + + def __init__(self, auth: AuthSchema) -> None: + self.auth = auth - @classmethod @require_superadmin - async def detail_service(cls, auth: AuthSchema, id: int) -> PackageOutSchema: - """ - 套餐详情 + async def detail(self, id: int) -> PackageOutSchema: + return await PackageCRUD(self.auth).get_or_404(id=id, out_schema=PackageOutSchema, msg="该数据不存在") - 参数: - - auth (AuthSchema): 认证信息模型 - - id (int): 套餐ID - - 返回: - - PackageOutSchema: 套餐详情 - """ - return await PackageCRUD(auth).get_or_404(id=id, out_schema=PackageOutSchema, msg="该数据不存在") - - @classmethod @require_superadmin - async def page_service( - cls, - auth: AuthSchema, + async def page( + self, page_no: int, page_size: int, search: PackageQueryParam | None = None, order_by: list[dict[str, str]] | None = None, ) -> dict: - """ - 分页查询套餐 - - 参数: - - auth (AuthSchema): 认证信息模型 - - page_no (int): 页码 - - page_size (int): 每页数量 - - search (PackageQueryParam | None): 查询参数 - - order_by (list[dict[str, str]] | None): 排序参数 - - 返回: - - dict: 分页数据 - """ - return await PackageCRUD(auth).page( + return await PackageCRUD(self.auth).page( offset=(page_no - 1) * page_size, limit=page_size, order_by=order_by or [{"sort": "asc"}, {"id": "asc"}], @@ -70,184 +45,90 @@ class PackageService: out_schema=PackageOutSchema, ) - @classmethod @require_superadmin - async def create_service(cls, auth: AuthSchema, data: PackageCreateSchema) -> PackageOutSchema: - """ - 创建套餐 - - 参数: - - auth (AuthSchema): 认证信息模型 - - data (PackageCreateSchema): 套餐创建模型 - - 返回: - - PackageOutSchema: 套餐详情 - """ - if await PackageCRUD(auth).get(name=data.name): + async def create(self, data: PackageCreateSchema) -> PackageOutSchema: + if await PackageCRUD(self.auth).get(name=data.name): raise CustomException(msg="创建失败,套餐名称已存在") - if await PackageCRUD(auth).get(code=data.code): + if await PackageCRUD(self.auth).get(code=data.code): raise CustomException(msg="创建失败,套餐编码已存在") - obj = await PackageCRUD(auth).create(data=data) + obj = await PackageCRUD(self.auth).create(data=data) result = PackageOutSchema.model_validate(obj) logger.info(f"创建套餐成功: {result.name}") return result - @classmethod @require_superadmin - async def update_service(cls, auth: AuthSchema, id: int, data: PackageUpdateSchema) -> PackageOutSchema: - """ - 更新套餐 - - 参数: - - auth (AuthSchema): 认证信息模型 - - id (int): 套餐ID - - data (PackageUpdateSchema): 套餐更新模型 - - 返回: - - PackageOutSchema: 套餐详情 - """ - obj = await PackageCRUD(auth).get_or_404(id=id) + async def update(self, id: int, data: PackageUpdateSchema) -> PackageOutSchema: + obj = await PackageCRUD(self.auth).get_or_404(id=id) if data.name is not None: - exist = await PackageCRUD(auth).get(name=data.name) + exist = await PackageCRUD(self.auth).get(name=data.name) if exist and exist.id != id: raise CustomException(msg="更新失败,名称重复") if data.code is not None: - exist = await PackageCRUD(auth).get(code=data.code) + exist = await PackageCRUD(self.auth).get(code=data.code) if exist and exist.id != id: raise CustomException(msg="更新失败,编码重复") - # 套餐禁用时记录受影响租户 if data.status is not None and data.status == 1 and obj.status == 0: - await cls.disable_cascade_service(auth=auth, package_id=id) - - updated = await PackageCRUD(auth).update(id=id, data=data) + await self.disable_cascade(package_id=id) + updated = await PackageCRUD(self.auth).update(id=id, data=data) return PackageOutSchema.model_validate(updated) - @classmethod @require_superadmin - async def delete_service(cls, auth: AuthSchema, ids: list[int]) -> None: - """ - 删除套餐 - - 参数: - - auth (AuthSchema): 认证信息模型 - - ids (list[int]): 套餐ID列表 - - 返回: - - None - """ + async def delete(self, ids: list[int]) -> None: if not ids: raise CustomException(msg="删除失败,删除对象不能为空") for pid in ids: stmt = select(func.count()).select_from(TenantModel).where(TenantModel.package_id == pid) - result = await auth.db.execute(stmt) + result = await self.auth.db.execute(stmt) count = result.scalar() if count and count > 0: raise CustomException(msg=f"套餐 ID={pid} 已被 {count} 个租户使用,无法删除") - await PackageCRUD(auth).delete(ids=ids) + await PackageCRUD(self.auth).delete(ids=ids) - @classmethod - async def disable_cascade_service(cls, auth: AuthSchema, package_id: int) -> None: - """ - 套餐禁用时日志记录受影响租户 - - 参数: - - auth (AuthSchema): 认证信息模型 - - package_id (int): 套餐ID - - 返回: - - None - """ + async def disable_cascade(self, package_id: int) -> None: stmt = select(TenantModel.id, TenantModel.name).where( TenantModel.package_id == package_id, TenantModel.status == 0, ) - result = await auth.db.execute(stmt) + result = await self.auth.db.execute(stmt) rows = result.all() if rows: tenant_ids = [row[0] for row in rows] logger.warning(f"套餐[{package_id}]已禁用,影响租户: {tenant_ids}") - @classmethod - async def get_menus_service(cls, auth: AuthSchema, package_id: int) -> list[int]: - """ - 获取套餐菜单权限 - - 参数: - - auth (AuthSchema): 认证信息模型 - - package_id (int): 套餐ID - - 返回: - - list[int]: menu_id 列表 - """ + async def get_menus(self, package_id: int) -> list[int]: stmt = select(PackageMenuModel.menu_id).where(PackageMenuModel.package_id == package_id) - result = await auth.db.execute(stmt) + result = await self.auth.db.execute(stmt) return [row[0] for row in result.all()] - @classmethod - async def set_menus_service(cls, auth: AuthSchema, package_id: int, data: PackageMenuSetSchema) -> None: - """ - 批量设置套餐菜单权限(先清空再写入) - - 参数: - - auth (AuthSchema): 认证信息模型 - - package_id (int): 套餐ID - - data (PackageMenuSetSchema): 菜单ID列表 - - 返回: - - None - """ - await auth.db.execute(sa.delete(PackageMenuModel).where(PackageMenuModel.package_id == package_id)) + async def set_menus(self, package_id: int, data: PackageMenuSetSchema) -> None: + await self.auth.db.execute(sa.delete(PackageMenuModel).where(PackageMenuModel.package_id == package_id)) for menu_id in data.menu_ids: - auth.db.add(PackageMenuModel(package_id=package_id, menu_id=menu_id)) - await auth.db.flush() + self.auth.db.add(PackageMenuModel(package_id=package_id, menu_id=menu_id)) + await self.auth.db.flush() logger.info(f"套餐[{package_id}]菜单权限已设置, count={len(data.menu_ids)}") - @classmethod - async def get_package_menu_ids(cls, auth: AuthSchema, package_id: int) -> list[int]: - """ - 获取套餐包含的菜单ID列表 - - 参数: - - auth (AuthSchema): 认证信息模型 - - package_id (int): 套餐ID - - 返回: - - list[int]: menu_id 列表 - """ + async def get_package_menu_ids(self, package_id: int) -> list[int]: stmt = select(PackageMenuModel.menu_id).where(PackageMenuModel.package_id == package_id) - result = await auth.db.execute(stmt) + result = await self.auth.db.execute(stmt) return [row[0] for row in result.all()] - @classmethod - async def get_tenant_available_menu_ids(cls, auth: AuthSchema, tenant_id: int) -> list[int]: - """ - 获取租户的完整可用菜单ID列表(仅从套餐菜单获取) - - 平台租户 (id=1) 返回全部启用菜单,不受套餐限制。 - - 参数: - - auth (AuthSchema): 认证信息模型 - - tenant_id (int): 租户ID - - 返回: - - list[int]: menu_id 列表 - """ + async def get_tenant_available_menu_ids(self, tenant_id: int) -> list[int]: from app.api.v1.module_platform.menu.model import MenuModel from app.api.v1.module_platform.tenant.model import TenantModel if tenant_id == 1: menu_stmt = select(MenuModel.id).where(MenuModel.status == 0) - result = await auth.db.execute(menu_stmt) + result = await self.auth.db.execute(menu_stmt) return [row[0] for row in result.all()] stmt = select(TenantModel).where(TenantModel.id == tenant_id).limit(1) - result = await auth.db.execute(stmt) + result = await self.auth.db.execute(stmt) tenant = result.scalar_one_or_none() if not tenant: return [] @@ -256,75 +137,41 @@ class PackageService: return [] pkg_stmt = select(PackageModel.status).where(PackageModel.id == tenant.package_id).limit(1) - pkg_result = await auth.db.execute(pkg_stmt) + pkg_result = await self.auth.db.execute(pkg_stmt) pkg_status = pkg_result.scalar_one_or_none() if pkg_status != 0: return [] menu_stmt = select(PackageMenuModel.menu_id).where(PackageMenuModel.package_id == tenant.package_id) - result = await auth.db.execute(menu_stmt) + result = await self.auth.db.execute(menu_stmt) return [row[0] for row in result.all()] - @classmethod - async def get_tenant_available_plugin_ids(cls, auth: AuthSchema, tenant_id: int) -> list[int]: - """ - 获取租户套餐可用的插件ID列表 - - 参数: - - auth (AuthSchema): 认证信息模型 - - tenant_id (int): 租户ID - - 返回: - - list[int]: plugin_id 列表 - """ + async def get_tenant_available_plugin_ids(self, tenant_id: int) -> list[int]: from app.api.v1.module_platform.tenant.model import TenantModel stmt = select(TenantModel).where(TenantModel.id == tenant_id).limit(1) - result = await auth.db.execute(stmt) + result = await self.auth.db.execute(stmt) tenant = result.scalar_one_or_none() if not tenant or not tenant.package_id: return [] pkg_stmt = select(PackageModel.status).where(PackageModel.id == tenant.package_id).limit(1) - pkg_result = await auth.db.execute(pkg_stmt) + pkg_result = await self.auth.db.execute(pkg_stmt) pkg_status = pkg_result.scalar_one_or_none() if pkg_status != 0: return [] plugin_stmt = select(PackagePluginModel.plugin_id).where(PackagePluginModel.plugin_id == tenant.package_id) - result = await auth.db.execute(plugin_stmt) + result = await self.auth.db.execute(plugin_stmt) return [row[0] for row in result.all()] - @classmethod - async def get_plugins_service(cls, auth: AuthSchema, package_id: int) -> list[int]: - """ - 获取套餐插件权限 - - 参数: - - auth (AuthSchema): 认证信息模型 - - package_id (int): 套餐ID - - 返回: - - list[int]: plugin_id 列表 - """ + async def get_plugins(self, package_id: int) -> list[int]: stmt = select(PackagePluginModel.plugin_id).where(PackagePluginModel.plugin_id == package_id) - result = await auth.db.execute(stmt) + result = await self.auth.db.execute(stmt) return [row[0] for row in result.all()] - @classmethod - async def set_plugins_service(cls, auth: AuthSchema, package_id: int, data: PackagePluginSetSchema) -> None: - """ - 批量设置套餐插件(先清空再写入) - - 参数: - - auth (AuthSchema): 认证信息模型 - - package_id (int): 套餐ID - - data (PackagePluginSetSchema): 插件ID列表 - - 返回: - - None - """ - await auth.db.execute(sa.delete(PackagePluginModel).where(PackagePluginModel.package_id == package_id)) + async def set_plugins(self, package_id: int, data: PackagePluginSetSchema) -> None: + await self.auth.db.execute(sa.delete(PackagePluginModel).where(PackagePluginModel.package_id == package_id)) for plugin_id in data.plugin_ids: - auth.db.add(PackagePluginModel(package_id=package_id, plugin_id=plugin_id)) - await auth.db.flush() + self.auth.db.add(PackagePluginModel(package_id=package_id, plugin_id=plugin_id)) + await self.auth.db.flush() diff --git a/backend/app/api/v1/module_platform/plugin/controller.py b/backend/app/api/v1/module_platform/plugin/controller.py index ff5ce39d..3c01a16c 100644 --- a/backend/app/api/v1/module_platform/plugin/controller.py +++ b/backend/app/api/v1/module_platform/plugin/controller.py @@ -20,12 +20,10 @@ from .schema import ( ) from .service import PluginService -PluginRouter = APIRouter(route_class=OperationLogRoute, prefix="/plugin", tags=["插件管理"]) +PluginRouter = APIRouter(route_class=OperationLogRoute, prefix="/plugin", tags=["平台管理", "插件管理"]) _PLUGIN_NS = "plugin" -# ───── 超管:插件 CRUD ───── - @PluginRouter.get( "/list", @@ -38,19 +36,7 @@ async def plugin_list_controller( search: Annotated[PluginQueryParam, Depends()], auth: Annotated[AuthSchema, Depends(AuthPermission(["module_platform:plugin:query"]))], ) -> JSONResponse: - """ - 插件列表 - - 参数: - - page (PaginationQueryParam): 分页查询参数。 - - search (PluginQueryParam): 查询筛选参数。 - - auth (AuthSchema): 认证信息模型。 - - 返回: - - JSONResponse: 包含分页插件列表的 JSON 响应。 - """ - r = await PluginService.page_service( - auth=auth, + r = await PluginService(auth).page( page_no=page.page_no, page_size=page.page_size, search=search, @@ -68,17 +54,7 @@ async def plugin_detail_controller( id: Annotated[int, Path()], auth: Annotated[AuthSchema, Depends(AuthPermission(["module_platform:plugin:query"]))], ) -> JSONResponse: - """ - 插件详情 - - 参数: - - id (int): 插件 ID。 - - auth (AuthSchema): 认证信息模型。 - - 返回: - - JSONResponse: 包含插件详情的 JSON 响应。 - """ - return SuccessResponse(data=await PluginService.detail_service(auth=auth, id=id), msg="查询成功") + return SuccessResponse(data=await PluginService(auth).detail(id=id), msg="查询成功") @PluginRouter.post( @@ -90,17 +66,7 @@ async def plugin_create_controller( data: PluginCreateSchema, auth: Annotated[AuthSchema, Depends(AuthPermission(["module_platform:plugin:create"]))], ) -> JSONResponse: - """ - 创建插件 - - 参数: - - data (PluginCreateSchema): 插件创建参数。 - - auth (AuthSchema): 认证信息模型。 - - 返回: - - JSONResponse: 包含创建后的插件详情的 JSON 响应。 - """ - r = await PluginService.create_service(auth=auth, data=data) + r = await PluginService(auth).create(data=data) await FastAPICache.clear(namespace=_PLUGIN_NS) return SuccessResponse(data=r, msg="创建成功") @@ -115,18 +81,7 @@ async def plugin_update_controller( data: PluginUpdateSchema, auth: Annotated[AuthSchema, Depends(AuthPermission(["module_platform:plugin:update"]))], ) -> JSONResponse: - """ - 更新插件 - - 参数: - - id (int): 插件 ID。 - - data (PluginUpdateSchema): 插件更新参数。 - - auth (AuthSchema): 认证信息模型。 - - 返回: - - JSONResponse: 包含更新后的插件详情的 JSON 响应。 - """ - r = await PluginService.update_service(auth=auth, id=id, data=data) + r = await PluginService(auth).update(id=id, data=data) await FastAPICache.clear(namespace=_PLUGIN_NS) return SuccessResponse(data=r, msg="更新成功") @@ -140,24 +95,11 @@ async def plugin_delete_controller( ids: Annotated[list[int], Body()], auth: Annotated[AuthSchema, Depends(AuthPermission(["module_platform:plugin:delete"]))], ) -> JSONResponse: - """ - 删除插件 - - 参数: - - ids (list[int]): 插件 ID 列表。 - - auth (AuthSchema): 认证信息模型。 - - 返回: - - JSONResponse: 删除结果。 - """ - await PluginService.delete_service(auth=auth, ids=ids) + await PluginService(auth).delete(ids=ids) await FastAPICache.clear(namespace=_PLUGIN_NS) return SuccessResponse(msg="删除成功") -# ───── 租户:插件市场 ───── - - @PluginRouter.get( "/marketplace", summary="插件市场", @@ -169,18 +111,7 @@ async def plugin_marketplace_controller( page: Annotated[PaginationQueryParam, Depends()], category: Annotated[str | None, Query(description="分类筛选")] = None, ) -> JSONResponse: - """ - 插件市场 - - 参数: - - page (PaginationQueryParam): 分页查询参数。 - - category (str | None): 分类筛选。 - - auth (AuthSchema): 认证信息模型。 - - 返回: - - JSONResponse: 包含分页市场插件列表的 JSON 响应。 - """ - r = await PluginService.marketplace_service(auth=auth, page_no=page.page_no, page_size=page.page_size, category=category) + r = await PluginService(auth).marketplace(page_no=page.page_no, page_size=page.page_size, category=category) return SuccessResponse(data=r, msg="查询成功") @@ -193,17 +124,7 @@ async def plugin_install_controller( data: PluginInstallSchema, auth: Annotated[AuthSchema, Depends(AuthPermission(["module_platform:plugin:install"]))], ) -> JSONResponse: - """ - 安装插件 - - 参数: - - data (PluginInstallSchema): 安装参数(插件 ID)。 - - auth (AuthSchema): 认证信息模型。 - - 返回: - - JSONResponse: 安装结果。 - """ - await PluginService.install_service(auth=auth, plugin_id=data.plugin_id) + await PluginService(auth).install(plugin_id=data.plugin_id) await FastAPICache.clear(namespace=_PLUGIN_NS) return SuccessResponse(msg="安装成功") @@ -217,17 +138,7 @@ async def plugin_uninstall_controller( data: PluginInstallSchema, auth: Annotated[AuthSchema, Depends(AuthPermission(["module_platform:plugin:uninstall"]))], ) -> JSONResponse: - """ - 卸载插件 - - 参数: - - data (PluginInstallSchema): 卸载参数(插件 ID)。 - - auth (AuthSchema): 认证信息模型。 - - 返回: - - JSONResponse: 卸载结果。 - """ - await PluginService.uninstall_service(auth=auth, plugin_id=data.plugin_id) + await PluginService(auth).uninstall(plugin_id=data.plugin_id) await FastAPICache.clear(namespace=_PLUGIN_NS) return SuccessResponse(msg="卸载成功") @@ -241,17 +152,7 @@ async def plugin_toggle_controller( data: PluginInstallSchema, auth: Annotated[AuthSchema, Depends(AuthPermission(["module_platform:plugin:toggle"]))], ) -> JSONResponse: - """ - 启用/禁用插件 - - 参数: - - data (PluginInstallSchema): 操作参数(插件 ID)。 - - auth (AuthSchema): 认证信息模型。 - - 返回: - - JSONResponse: 操作结果。 - """ - await PluginService.toggle_service(auth=auth, plugin_id=data.plugin_id) + await PluginService(auth).toggle(plugin_id=data.plugin_id) await FastAPICache.clear(namespace=_PLUGIN_NS) return SuccessResponse(msg="操作成功") @@ -265,16 +166,7 @@ async def plugin_toggle_controller( async def plugin_my_list_controller( auth: Annotated[AuthSchema, Depends(AuthPermission(["module_platform:plugin:query"]))], ) -> JSONResponse: - """ - 我的插件 - - 参数: - - auth (AuthSchema): 认证信息模型。 - - 返回: - - JSONResponse: 包含已安装插件列表的 JSON 响应。 - """ - return SuccessResponse(data=await PluginService.my_plugins_service(auth=auth), msg="查询成功") + return SuccessResponse(data=await PluginService(auth).my_plugins(), msg="查询成功") @PluginRouter.post( @@ -285,18 +177,6 @@ async def plugin_my_list_controller( async def plugin_reload_controller( auth: Annotated[AuthSchema, Depends(AuthPermission(["module_platform:plugin:reload"]))], ) -> JSONResponse: - """ - 热重载插件路由(管理员)。 - - 重新扫描 app/plugin/module_* 目录,清除模块缓存并注册新路由, - 无需重启服务器。 - - 参数: - - auth (AuthSchema): 认证信息模型。 - - 返回: - - JSONResponse: 包含重载结果的 JSON 响应。 - """ - msg = PluginService.reload_service() + msg = PluginService.reload() await FastAPICache.clear(namespace=_PLUGIN_NS) return SuccessResponse(data=msg, msg="重载成功") diff --git a/backend/app/api/v1/module_platform/plugin/service.py b/backend/app/api/v1/module_platform/plugin/service.py index f2e5486b..ba07ad5b 100644 --- a/backend/app/api/v1/module_platform/plugin/service.py +++ b/backend/app/api/v1/module_platform/plugin/service.py @@ -13,34 +13,20 @@ from .schema import PluginCreateSchema, PluginOutSchema, PluginQueryParam, Plugi class PluginService: - """ - 插件管理服务(仅超级管理员可操作 CRUD,租户通过 marketplace/install/uninstall/toggle/my 操作) - """ + """插件管理服务(仅超级管理员可操作 CRUD,租户通过 marketplace/install/uninstall/toggle/my 操作)""" + + def __init__(self, auth: AuthSchema) -> None: + self.auth = auth - @classmethod @require_superadmin - async def page_service( - cls, - auth: AuthSchema, + async def page( + self, page_no: int, page_size: int, search: PluginQueryParam | None = None, order_by: list | None = None, ) -> dict: - """ - 分页查询插件 - - 参数: - - auth (AuthSchema): 认证信息模型 - - page_no (int): 页码 - - page_size (int): 每页数量 - - search (PluginQueryParam | None): 查询参数 - - order_by (list | None): 排序参数 - - 返回: - - dict: 分页数据 - """ - return await PluginCRUD(auth).page( + return await PluginCRUD(self.auth).page( offset=(page_no - 1) * page_size, limit=page_size, order_by=order_by or [{"sort": "asc"}], @@ -48,102 +34,41 @@ class PluginService: out_schema=PluginOutSchema, ) - @classmethod @require_superadmin - async def detail_service(cls, auth: AuthSchema, id: int) -> PluginOutSchema: - """ - 插件详情 + async def detail(self, id: int) -> PluginOutSchema: + return await PluginCRUD(self.auth).get_or_404(id=id, out_schema=PluginOutSchema) - 参数: - - auth (AuthSchema): 认证信息模型 - - id (int): 插件ID - - 返回: - - PluginOutSchema: 插件详情 - """ - return await PluginCRUD(auth).get_or_404(id=id, out_schema=PluginOutSchema) - - @classmethod @require_superadmin - async def create_service(cls, auth: AuthSchema, data: PluginCreateSchema) -> PluginOutSchema: - """ - 创建插件 - - 参数: - - auth (AuthSchema): 认证信息模型 - - data (PluginCreateSchema): 插件创建模型 - - 返回: - - PluginOutSchema: 插件详情 - """ - if await PluginCRUD(auth).get(code=data.code): + async def create(self, data: PluginCreateSchema) -> PluginOutSchema: + if await PluginCRUD(self.auth).get(code=data.code): raise CustomException(msg="创建失败,插件编码已存在") - obj = await PluginCRUD(auth).create(data=data) + obj = await PluginCRUD(self.auth).create(data=data) return PluginOutSchema.model_validate(obj) - @classmethod @require_superadmin - async def update_service(cls, auth: AuthSchema, id: int, data: PluginUpdateSchema) -> PluginOutSchema: - """ - 更新插件 - - 参数: - - auth (AuthSchema): 认证信息模型 - - id (int): 插件ID - - data (PluginUpdateSchema): 插件更新模型 - - 返回: - - PluginOutSchema: 插件详情 - """ - _ = await PluginCRUD(auth).get_or_404(id=id) - updated = await PluginCRUD(auth).update(id=id, data=data) + async def update(self, id: int, data: PluginUpdateSchema) -> PluginOutSchema: + _ = await PluginCRUD(self.auth).get_or_404(id=id) + updated = await PluginCRUD(self.auth).update(id=id, data=data) return PluginOutSchema.model_validate(updated) - @classmethod @require_superadmin - async def delete_service(cls, auth: AuthSchema, ids: list[int]) -> None: - """ - 删除插件 + async def delete(self, ids: list[int]) -> None: + await PluginCRUD(self.auth).delete(ids=ids) - 参数: - - auth (AuthSchema): 认证信息模型 - - ids (list[int]): 插件ID列表 - - 返回: - - None - """ - await PluginCRUD(auth).delete(ids=ids) - - # ───── 插件市场 API ───── - - @classmethod - async def marketplace_service(cls, auth: AuthSchema, page_no: int, page_size: int, category: str | None = None) -> dict: - """ - 插件市场列表(租户端) - - 参数: - - auth (AuthSchema): 认证信息模型 - - page_no (int): 页码 - - page_size (int): 每页数量 - - category (str | None): 分类筛选 - - 返回: - - dict: 分页数据(含租户的 installed/purchased 标记) - """ - search = {} + async def marketplace(self, page_no: int, page_size: int, category: str | None = None) -> dict: + search: dict = {"status": ("eq", "0")} if category: search["category"] = ("eq", category) - search["status"] = ("eq", "0") - result = await PluginCRUD(auth).page( + result = await PluginCRUD(self.auth).page( offset=(page_no - 1) * page_size, limit=page_size, order_by=[{"sort": "asc"}], search=search, out_schema=PluginOutSchema, ) - tenant_id = getattr(auth, "tenant_id", None) or auth.user.tenant_id + tenant_id = getattr(self.auth, "tenant_id", None) or self.auth.user.tenant_id if tenant_id and result.items: - records = await auth.db.execute( + records = await self.auth.db.execute( sa.select(TenantPluginModel.plugin_id, TenantPluginModel.purchased).where( TenantPluginModel.tenant_id == tenant_id, ) @@ -155,38 +80,24 @@ class PluginService: item["purchased"] = record_map.get(pid, False) return result - @classmethod - async def install_service(cls, auth: AuthSchema, plugin_id: int) -> None: - """ - 安装插件 - - 参数: - - auth (AuthSchema): 认证信息模型 - - plugin_id (int): 插件ID - - 返回: - - None - """ - tenant_id = getattr(auth, "tenant_id", None) or auth.user.tenant_id + async def install(self, plugin_id: int) -> None: + tenant_id = getattr(self.auth, "tenant_id", None) or self.auth.user.tenant_id if not tenant_id: raise CustomException(msg="无法获取租户信息") - # 平台租户不限制插件安装 if tenant_id != 1: from app.api.v1.module_platform.package.service import PackageService - allowed_plugin_ids = await PackageService.get_tenant_available_plugin_ids(auth, tenant_id) - # 套餐有插件配置时,仅允许安装套餐内的插件 + allowed_plugin_ids = await PackageService(self.auth).get_tenant_available_plugin_ids(tenant_id) if allowed_plugin_ids and plugin_id not in allowed_plugin_ids: raise CustomException(msg="当前套餐不支持安装此插件") - plugin = await PluginCRUD(auth).get(id=plugin_id) + plugin = await PluginCRUD(self.auth).get(id=plugin_id) if not plugin or plugin.status == 1: raise CustomException(msg="该数据不存在") - # 付费插件需要先购买 if tenant_id != 1 and getattr(plugin, "price", 0) > 0: - exist = await auth.db.execute( + exist = await self.auth.db.execute( sa.select(TenantPluginModel) .where( TenantPluginModel.tenant_id == tenant_id, @@ -198,7 +109,7 @@ class PluginService: if not tp_record or not tp_record.purchased: raise CustomException(msg="此插件为付费插件,请先购买后再安装") - exist = await auth.db.execute( + exist = await self.auth.db.execute( sa.select(TenantPluginModel) .where( TenantPluginModel.tenant_id == tenant_id, @@ -207,7 +118,7 @@ class PluginService: .limit(1) ) if exist.scalar_one_or_none(): - await auth.db.execute( + await self.auth.db.execute( sa.update(TenantPluginModel) .where( TenantPluginModel.tenant_id == tenant_id, @@ -223,48 +134,26 @@ class PluginService: purchased=True if getattr(plugin, "price", 0) == 0 else False, installed_time=datetime.now(), ) - auth.db.add(tp) - await auth.db.flush() + self.auth.db.add(tp) + await self.auth.db.flush() logger.info(f"租户[{tenant_id}]安装插件[{plugin.name}]") - @classmethod - async def uninstall_service(cls, auth: AuthSchema, plugin_id: int) -> None: - """ - 卸载插件 - - 参数: - - auth (AuthSchema): 认证信息模型 - - plugin_id (int): 插件ID - - 返回: - - None - """ - tenant_id = getattr(auth, "tenant_id", None) or auth.user.tenant_id + async def uninstall(self, plugin_id: int) -> None: + tenant_id = getattr(self.auth, "tenant_id", None) or self.auth.user.tenant_id if not tenant_id: raise CustomException(msg="无法获取租户信息") - await auth.db.execute( + await self.auth.db.execute( sa.delete(TenantPluginModel).where( TenantPluginModel.tenant_id == tenant_id, TenantPluginModel.plugin_id == plugin_id, ) ) - await auth.db.flush() + await self.auth.db.flush() logger.info(f"租户[{tenant_id}]卸载插件[{plugin_id}]") - @classmethod - async def toggle_service(cls, auth: AuthSchema, plugin_id: int) -> None: - """ - 启用/禁用插件 - - 参数: - - auth (AuthSchema): 认证信息模型 - - plugin_id (int): 插件ID - - 返回: - - None - """ - tenant_id = getattr(auth, "tenant_id", None) or auth.user.tenant_id - tp = await auth.db.execute( + async def toggle(self, plugin_id: int) -> None: + tenant_id = getattr(self.auth, "tenant_id", None) or self.auth.user.tenant_id + tp = await self.auth.db.execute( sa.select(TenantPluginModel) .where( TenantPluginModel.tenant_id == tenant_id, @@ -276,24 +165,14 @@ class PluginService: if not tp: raise CustomException(msg="未安装该插件") tp.enabled = not tp.enabled - await auth.db.flush() + await self.auth.db.flush() logger.info(f"租户[{tenant_id}]插件[{plugin_id}]状态→{tp.enabled}") - @classmethod - async def my_plugins_service(cls, auth: AuthSchema) -> list[dict]: - """ - 查询我的插件列表(租户端) - - 参数: - - auth (AuthSchema): 认证信息模型 - - 返回: - - list[dict]: 已安装插件列表 - """ - tenant_id = getattr(auth, "tenant_id", None) or auth.user.tenant_id + async def my_plugins(self) -> list[dict]: + tenant_id = getattr(self.auth, "tenant_id", None) or self.auth.user.tenant_id if not tenant_id: return [] - result = await auth.db.execute( + result = await self.auth.db.execute( sa.select(PluginModel, TenantPluginModel).join(TenantPluginModel, TenantPluginModel.plugin_id == PluginModel.id).where(TenantPluginModel.tenant_id == tenant_id).order_by(PluginModel.sort) ) plugins = [] @@ -305,19 +184,8 @@ class PluginService: plugins.append(d) return plugins - # ───── 插件热重载 ───── - - @classmethod - def reload_service(cls) -> str: - """ - 热重载插件路由(同步函数,直接操作模块缓存与 app.routes)。 - - 要求 init_app.py 已在启动时调用 discover.set_app_ref(app), - 否则仅重建 Router 对象,不会更新运行中的 app 路由。 - - 返回: - - str: 重载结果描述 - """ + @staticmethod + def reload() -> str: from app.core.discover import reload_dynamic_router router = reload_dynamic_router() diff --git a/backend/app/api/v1/module_platform/self_service/controller.py b/backend/app/api/v1/module_platform/self_service/controller.py index 9af98d9e..b543dd90 100644 --- a/backend/app/api/v1/module_platform/self_service/controller.py +++ b/backend/app/api/v1/module_platform/self_service/controller.py @@ -29,7 +29,6 @@ TenantSelfServiceRouter = APIRouter( tags=["自助服务"], ) - @TenantSelfServiceRouter.get( "/package/available", summary="可选套餐列表", @@ -38,16 +37,9 @@ TenantSelfServiceRouter = APIRouter( async def package_available_controller( auth: Annotated[AuthSchema, Depends(AuthPermission(["tenant:package:query"]))], ) -> JSONResponse: - """ - 可选套餐列表 - - 返回: - - JSONResponse: 包含可用套餐列表的 JSON 响应。 - """ result = await SelfService.get_available_packages(auth=auth, tenant_id=auth.tenant_id) return SuccessResponse(data=result, msg="查询成功") - @TenantSelfServiceRouter.get( "/package/preview", summary="套餐变更影响预览", @@ -57,19 +49,9 @@ async def package_preview_controller( target_package_id: Annotated[int, Query(ge=1, description="目标套餐ID")], auth: Annotated[AuthSchema, Depends(AuthPermission(["tenant:package:query"]))], ) -> JSONResponse: - """ - 套餐变更影响预览 - - 参数: - - target_package_id (int): 目标套餐 ID。 - - 返回: - - JSONResponse: 包含套餐变更影响预览的 JSON 响应。 - """ result = await SelfService.preview_package_change(auth=auth, tenant_id=auth.tenant_id, target_package_id=target_package_id) return SuccessResponse(data=result, msg="查询成功") - @TenantSelfServiceRouter.post( "/order/create", summary="创建自助订单", @@ -79,19 +61,9 @@ async def order_create_controller( data: SelfOrderCreate, auth: Annotated[AuthSchema, Depends(AuthPermission(["tenant:order:create"]))], ) -> JSONResponse: - """ - 创建自助订单 - - 参数: - - data (SelfOrderCreate): 订单创建参数。 - - 返回: - - JSONResponse: 包含订单详情的 JSON 响应。 - """ result = await SelfService.create_self_order(auth=auth, tenant_id=auth.tenant_id, data=data) return SuccessResponse(data=result, msg="订单创建成功") - @TenantSelfServiceRouter.post( "/plugin/purchase", summary="购买付费插件", @@ -101,19 +73,9 @@ async def plugin_purchase_controller( data: PluginPurchaseCreate, auth: Annotated[AuthSchema, Depends(AuthPermission(["tenant:order:create"]))], ) -> JSONResponse: - """ - 购买付费插件 - - 参数: - - data (PluginPurchaseCreate): 插件购买参数。 - - 返回: - - JSONResponse: 包含订单详情的 JSON 响应。 - """ result = await SelfService.create_plugin_purchase_order(auth=auth, tenant_id=auth.tenant_id, data=data) return SuccessResponse(data=result, msg="插件订单创建成功") - @TenantSelfServiceRouter.get( "/order/list", summary="我的订单列表", @@ -123,15 +85,6 @@ async def order_list_controller( page: Annotated[PaginationQueryParam, Depends()], auth: Annotated[AuthSchema, Depends(AuthPermission(["tenant:order:query"]))], ) -> JSONResponse: - """ - 我的订单列表 - - 参数: - - page (PaginationQueryParam): 分页查询参数。 - - 返回: - - JSONResponse: 包含分页订单列表的 JSON 响应。 - """ result = await SelfService.get_self_order_list( auth=auth, tenant_id=auth.tenant_id, @@ -141,7 +94,6 @@ async def order_list_controller( ) return SuccessResponse(data=result, msg="查询成功") - @TenantSelfServiceRouter.get( "/order/detail/{order_id}", summary="订单详情", @@ -151,19 +103,9 @@ async def order_detail_controller( order_id: Annotated[int, Path(ge=1, description="订单ID")], auth: Annotated[AuthSchema, Depends(AuthPermission(["tenant:order:query"]))], ) -> JSONResponse: - """ - 订单详情 - - 参数: - - order_id (int): 订单 ID。 - - 返回: - - JSONResponse: 包含订单详情的 JSON 响应。 - """ - result = await SelfService.get_self_order_detail(auth=auth, tenant_id=auth.tenant_id, order_id=order_id) + result = await SelfService.get_self_order_detail(auth=auth, order_id=order_id) return SuccessResponse(data=result, msg="查询成功") - @TenantSelfServiceRouter.get( "/workspace", summary="租户工作台概览", @@ -172,11 +114,5 @@ async def order_detail_controller( async def tenant_workspace_controller( auth: Annotated[AuthSchema, Depends(get_current_user)], ) -> JSONResponse: - """ - 租户工作台概览 - - 返回: - - JSONResponse: 包含工作台概览信息的 JSON 响应。 - """ result = await SelfService.get_workspace_data(auth=auth, tenant_id=auth.tenant_id) return SuccessResponse(data=result, msg="查询成功") diff --git a/backend/app/api/v1/module_platform/self_service/service.py b/backend/app/api/v1/module_platform/self_service/service.py index 2f66253c..6200f86c 100644 --- a/backend/app/api/v1/module_platform/self_service/service.py +++ b/backend/app/api/v1/module_platform/self_service/service.py @@ -355,13 +355,12 @@ class SelfService: ) @classmethod - async def get_self_order_detail(cls, auth: AuthSchema, tenant_id: int, order_id: int) -> SelfOrderDetailOut: + async def get_self_order_detail(cls, auth: AuthSchema, order_id: int) -> SelfOrderDetailOut: """ 订单详情 参数: - auth (AuthSchema): 认证信息模型 - - tenant_id (int): 租户ID - order_id (int): 订单ID 返回: @@ -407,12 +406,10 @@ class SelfService: quota=WorkspaceQuotaInfo(), ) - # ── 套餐 ── package = None if tenant.package_id: package = await auth.db.get(PackageModel, tenant.package_id) - # ── 用量计数 ── async def _count(model_cls) -> int: stmt = ( select(func.count()) @@ -428,7 +425,6 @@ class SelfService: role_count = await _count(RoleModel) dept_count = await _count(DeptModel) - # ── 到期状态 ── now = datetime.now() days_remaining = (tenant.end_time - now).days if tenant.end_time else 0 @@ -441,7 +437,6 @@ class SelfService: "5": "已归档", } - # ── 近期订单 ── orders_stmt = select(OrderModel).where(OrderModel.tenant_id == tenant_id).order_by(OrderModel.created_time.desc()).limit(5) orders_result = await auth.db.execute(orders_stmt) recent_orders = [] diff --git a/backend/app/api/v1/module_platform/tenant/controller.py b/backend/app/api/v1/module_platform/tenant/controller.py index 9a673f2c..211604a6 100644 --- a/backend/app/api/v1/module_platform/tenant/controller.py +++ b/backend/app/api/v1/module_platform/tenant/controller.py @@ -26,11 +26,10 @@ from .schema import ( ) from .service import TenantService -TenantRouter = APIRouter(route_class=OperationLogRoute, prefix="/tenant", tags=["租户管理"]) +TenantRouter = APIRouter(route_class=OperationLogRoute, prefix="/tenant", tags=["平台管理", "租户管理"]) _TENANT_NS = "tenant" - @TenantRouter.get( "/detail/{id}", summary="获取租户详情", @@ -41,19 +40,9 @@ async def get_obj_detail_controller( id: Annotated[int, Path(description="租户ID")], auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:tenant:query"]))], ) -> JSONResponse: - """ - 获取租户详情 - - 参数: - - id (int): 租户 ID。 - - 返回: - - JSONResponse: 包含租户详情的 JSON 响应。 - """ - result_dict = await TenantService.detail_service(id=id, auth=auth) + result_dict = await TenantService(auth).detail(id=id) return SuccessResponse(data=result_dict, msg="获取租户详情成功") - @TenantRouter.get( "/list", summary="查询租户列表", @@ -64,21 +53,10 @@ async def get_obj_list_controller( search: Annotated[TenantQueryParam, Depends()], auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:tenant:query"]))], ) -> JSONResponse: - """ - 查询租户列表 - - 参数: - - page (PaginationQueryParam): 分页查询参数。 - - search (TenantQueryParam): 查询筛选参数。 - - 返回: - - JSONResponse: 包含分页租户列表的 JSON 响应。 - """ order_by = [{"id": "asc"}] if page.order_by: order_by = page.order_by - result_dict = await TenantService.page_service( - auth=auth, + result_dict = await TenantService(auth).page( page_no=page.page_no if page.page_no is not None else 1, page_size=page.page_size if page.page_size is not None else 10, search=search, @@ -86,7 +64,6 @@ async def get_obj_list_controller( ) return SuccessResponse(data=result_dict, msg="查询租户列表成功") - @TenantRouter.post( "/create", summary="创建租户", @@ -96,20 +73,10 @@ async def create_obj_controller( data: TenantCreateSchema, auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:tenant:create"]))], ) -> JSONResponse: - """ - 创建租户 - - 参数: - - data (TenantCreateSchema): 租户创建参数。 - - 返回: - - JSONResponse: 包含创建后的租户详情的 JSON 响应。 - """ - result_dict = await TenantService.create_service(auth=auth, data=data) + result_dict = await TenantService(auth).create(data=data) await FastAPICache.clear(namespace=_TENANT_NS) return SuccessResponse(data=result_dict, msg="创建租户成功") - @TenantRouter.put( "/update/{id}", summary="修改租户", @@ -120,21 +87,10 @@ async def update_obj_controller( id: Annotated[int, Path(description="租户ID")], auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:tenant:update"]))], ) -> JSONResponse: - """ - 修改租户 - - 参数: - - id (int): 租户 ID。 - - data (TenantUpdateSchema): 租户更新参数。 - - 返回: - - JSONResponse: 包含更新后的租户详情的 JSON 响应。 - """ - result_dict = await TenantService.update_service(auth=auth, id=id, data=data) + result_dict = await TenantService(auth).update(id=id, data=data) await FastAPICache.clear(namespace=_TENANT_NS) return SuccessResponse(data=result_dict, msg="修改租户成功") - @TenantRouter.delete( "/delete", summary="删除租户", @@ -144,20 +100,10 @@ async def delete_obj_controller( ids: Annotated[list[int], Body(..., description="ID列表")], auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:tenant:delete"]))], ) -> JSONResponse: - """ - 删除租户 - - 参数: - - ids (list[int]): 租户 ID 列表。 - - 返回: - - JSONResponse: 删除结果。 - """ - await TenantService.delete_service(auth=auth, ids=ids) + await TenantService(auth).delete(ids=ids) await FastAPICache.clear(namespace=_TENANT_NS) return SuccessResponse(msg="删除租户成功") - @TenantRouter.patch( "/status/batch", summary="批量修改租户状态", @@ -167,20 +113,10 @@ async def batch_set_available_obj_controller( data: BatchSetAvailable, auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:tenant:patch"]))], ) -> JSONResponse: - """ - 批量修改租户状态 - - 参数: - - data (BatchSetAvailable): 批量状态设置参数。 - - 返回: - - JSONResponse: 操作结果。 - """ - await TenantService.set_available_service(auth=auth, data=data) + await TenantService(auth).set_available(data=data) await FastAPICache.clear(namespace=_TENANT_NS) return SuccessResponse(msg="批量修改租户状态成功") - @TenantRouter.put( "/status/{id}", summary="启/禁用租户", @@ -190,20 +126,10 @@ async def toggle_tenant_status_controller( id: Annotated[int, Path(description="租户ID")], auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:tenant:patch"]))], ) -> JSONResponse: - """ - 启/禁用租户 - - 参数: - - id (int): 租户 ID。 - - 返回: - - JSONResponse: 操作结果。 - """ - await TenantService.toggle_status_service(auth=auth, id=id) + await TenantService(auth).toggle_status(id=id) await FastAPICache.clear(namespace=_TENANT_NS) return SuccessResponse(msg="修改租户状态成功") - @TenantRouter.get( "/{id}/users", summary="获取租户用户列表", @@ -214,19 +140,9 @@ async def get_tenant_users_controller( id: Annotated[int, Path(description="租户ID")], auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:tenant:query"]))], ) -> JSONResponse: - """ - 获取租户用户列表 - - 参数: - - id (int): 租户 ID。 - - 返回: - - JSONResponse: 包含租户用户列表的 JSON 响应。 - """ - result = await TenantService.get_tenant_users_service(auth=auth, tenant_id=id) + result = await TenantService(auth).get_tenant_users(tenant_id=id) return SuccessResponse(data=result, msg="获取租户用户列表成功") - @TenantRouter.post( "/{id}/users", summary="向租户添加用户", @@ -237,21 +153,10 @@ async def add_tenant_user_controller( data: TenantUserAddSchema, auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:tenant:create"]))], ) -> JSONResponse: - """ - 向租户添加用户 - - 参数: - - id (int): 租户 ID。 - - data (TenantUserAddSchema): 用户添加参数。 - - 返回: - - JSONResponse: 操作结果。 - """ - await TenantService.add_tenant_user_service(auth=auth, tenant_id=id, data=data) + await TenantService(auth).add_tenant_user(tenant_id=id, data=data) await FastAPICache.clear(namespace=_TENANT_NS) return SuccessResponse(msg="添加用户成功") - @TenantRouter.delete( "/{id}/users/{uid}", summary="从租户移除用户", @@ -262,24 +167,10 @@ async def remove_tenant_user_controller( uid: Annotated[int, Path(description="用户ID")], auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:tenant:delete"]))], ) -> JSONResponse: - """ - 从租户移除用户 - - 参数: - - id (int): 租户 ID。 - - uid (int): 用户 ID。 - - 返回: - - JSONResponse: 操作结果。 - """ - await TenantService.remove_tenant_user_service(auth=auth, tenant_id=id, user_id=uid) + await TenantService(auth).remove_tenant_user(tenant_id=id, user_id=uid) await FastAPICache.clear(namespace=_TENANT_NS) return SuccessResponse(msg="移除用户成功") - -# ============ P1: 租户配置 ============ - - @TenantRouter.get( "/{id}/config", summary="获取租户配置", @@ -289,19 +180,9 @@ async def get_tenant_config_controller( id: Annotated[int, Path(description="租户ID")], auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:tenant:query"]))], ) -> JSONResponse: - """ - 获取租户配置 - - 参数: - - id (int): 租户 ID。 - - 返回: - - JSONResponse: 包含租户配置列表的 JSON 响应。 - """ - result = await TenantService.get_config_items_service(auth=auth, tenant_id=id) + result = await TenantService(auth).get_config_items(tenant_id=id) return SuccessResponse(data=result, msg="获取租户配置成功") - @TenantRouter.get( "/{id}/config/info", summary="获取租户配置(公开-缓存)", @@ -311,19 +192,9 @@ async def get_tenant_config_info_controller( id: Annotated[int, Path(description="租户ID")], redis: Annotated[Redis, Depends(redis_getter)], ) -> JSONResponse: - """ - 获取租户配置(公开-缓存) - - 参数: - - id (int): 租户 ID。 - - 返回: - - JSONResponse: 包含租户配置列表的 JSON 响应。 - """ - result = await TenantService.get_config_cache_items_service(redis=redis, tenant_id=id) + result = await TenantService.get_config_cache_items(redis=redis, tenant_id=id) return SuccessResponse(data=result, msg="获取租户配置成功") - @TenantRouter.put( "/{id}/config", summary="更新租户配置", @@ -335,24 +206,10 @@ async def update_tenant_config_controller( redis: Annotated[Redis, Depends(redis_getter)], auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:tenant:update"]))], ) -> JSONResponse: - """ - 更新租户配置 - - 参数: - - id (int): 租户 ID。 - - data (list[TenantConfigItem]): 配置项列表。 - - 返回: - - JSONResponse: 包含更新后的配置列表的 JSON 响应。 - """ - result = await TenantService.update_config_service(auth=auth, redis=redis, tenant_id=id, config=data) + result = await TenantService(auth).update_config(redis=redis, tenant_id=id, config=data) await FastAPICache.clear(namespace=_TENANT_NS) return SuccessResponse(data=result, msg="更新租户配置成功") - -# ============ 续期 ============ - - @TenantRouter.put( "/renew/{id}", summary="租户续期", @@ -363,24 +220,10 @@ async def renew_tenant_controller( data: TenantRenewSchema, auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:tenant:update"]))], ) -> JSONResponse: - """ - 租户续期 - - 参数: - - id (int): 租户 ID。 - - data (TenantRenewSchema): 续期时间参数。 - - 返回: - - JSONResponse: 包含续期后的租户详情的 JSON 响应。 - """ - result = await TenantService.renew_service(auth=auth, tenant_id=id, end_time=data.end_time) + result = await TenantService(auth).renew(tenant_id=id, end_time=data.end_time) await FastAPICache.clear(namespace=_TENANT_NS) return SuccessResponse(data=result, msg="租户续期成功") - -# ============ 套餐变更预览 ============ - - @TenantRouter.get( "/{id}/package-change-preview", summary="套餐变更影响预览", @@ -391,15 +234,5 @@ async def package_change_preview_controller( new_package_id: Annotated[int, Query(..., description="目标套餐ID")], auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:tenant:query"]))], ) -> JSONResponse: - """ - 套餐变更影响预览 - - 参数: - - id (int): 租户 ID。 - - new_package_id (int): 目标套餐 ID。 - - 返回: - - JSONResponse: 包含套餐变更影响预览的 JSON 响应。 - """ - result = await TenantService.package_change_preview_service(auth=auth, tenant_id=id, new_package_id=new_package_id) + result = await TenantService(auth).package_change_preview(tenant_id=id, new_package_id=new_package_id) return SuccessResponse(data=result, msg="套餐变更预览成功") diff --git a/backend/app/api/v1/module_platform/tenant/schema.py b/backend/app/api/v1/module_platform/tenant/schema.py index 44eb0a76..12a27c4e 100644 --- a/backend/app/api/v1/module_platform/tenant/schema.py +++ b/backend/app/api/v1/module_platform/tenant/schema.py @@ -203,9 +203,6 @@ class TenantUserOutSchema(BaseModel): name: str = Field(default="", description="用户姓名") -# ============ P1: 租户配置 ============ - - class TenantConfigItem(BaseModel): """租户配置项""" @@ -222,9 +219,6 @@ class TenantConfigOutSchema(BaseModel): config_value: str | None = Field(default=None, description="配置值") -# ============ 续期 ============ - - class TenantRenewSchema(BaseModel): """租户续期""" @@ -239,9 +233,6 @@ class TenantRenewSchema(BaseModel): return self -# ============ 套餐变更预览 ============ - - class PackageChangePreviewOut(BaseModel): """套餐变更影响预览响应""" diff --git a/backend/app/api/v1/module_platform/tenant/service.py b/backend/app/api/v1/module_platform/tenant/service.py index 1c84e56f..a556bd43 100644 --- a/backend/app/api/v1/module_platform/tenant/service.py +++ b/backend/app/api/v1/module_platform/tenant/service.py @@ -30,32 +30,35 @@ from .schema import ( class TenantService: """ 租户管理服务(查询操作租户可见,写操作仅超级管理员可操作) + + 设计:实例方法承载「当前用户上下文 (auth)」,``redis`` 仍是方法参数。 + 内部跨方法调用从 ``cls.xxx(auth, ...)`` 改为 ``self.xxx(...)``。 + 定时任务方法与静态工具方法保持 ``@staticmethod``(无 auth)。 """ - @classmethod - async def detail_service(cls, auth: AuthSchema, id: int) -> TenantOutSchema: + def __init__(self, auth: AuthSchema) -> None: + self.auth = auth + + async def detail(self, id: int) -> TenantOutSchema: """ 租户详情 参数: - - auth (AuthSchema): 认证信息模型 - id (int): 租户ID 返回: - TenantOutSchema: 租户详情 """ - return await TenantCRUD(auth).get_or_404(id=id, out_schema=TenantOutSchema) + return await TenantCRUD(self.auth).get_or_404(id=id, out_schema=TenantOutSchema) - @classmethod - async def page_service( - cls, - auth: AuthSchema, + async def page( + self, page_no: int, page_size: int, search: TenantQueryParam | None = None, order_by: list[dict[str, str]] | None = None, ) -> dict: - return await TenantCRUD(auth).page( + return await TenantCRUD(self.auth).page( offset=(page_no - 1) * page_size, limit=page_size, order_by=order_by or [{"id": "asc"}], @@ -63,15 +66,14 @@ class TenantService: out_schema=TenantOutSchema, ) - @classmethod @require_superadmin - async def create_service(cls, auth: AuthSchema, data: TenantCreateSchema) -> TenantOutSchema: - if await TenantCRUD(auth).get(name=data.name): + async def create(self, data: TenantCreateSchema) -> TenantOutSchema: + if await TenantCRUD(self.auth).get(name=data.name): raise CustomException(msg="创建失败,名称已存在") - if await TenantCRUD(auth).get(code=data.code): + if await TenantCRUD(self.auth).get(code=data.code): raise CustomException(msg="创建失败,编码已存在") - tenant_obj = await TenantCRUD(auth).create(data=data) + tenant_obj = await TenantCRUD(self.auth).create(data=data) if not tenant_obj: raise CustomException(msg="创建租户失败") @@ -82,7 +84,7 @@ class TenantService: username = f"{tenant_obj.code}_admin" from app.api.v1.module_system.user.crud import UserCRUD - if await UserCRUD(auth).get(username=username): + if await UserCRUD(self.auth).get(username=username): raise CustomException(msg=f"初始管理员用户名已存在: {username},请更换租户编码后重试") password_length = 12 @@ -97,7 +99,7 @@ class TenantService: "is_superuser": False, } try: - user_obj = await UserCRUD(auth).create(data=admin_data) + user_obj = await UserCRUD(self.auth).create(data=admin_data) if not user_obj: raise CustomException(msg="创建租户初始管理员失败") except CustomException: @@ -108,26 +110,24 @@ class TenantService: logger.info(f"为租户[{tenant_obj.name}]创建初始管理员成功,用户名: {username},临时密码: {password}") - await auth.db.refresh(tenant_obj) + await self.auth.db.refresh(tenant_obj) result = TenantOutSchema.model_validate(tenant_obj) return result - @classmethod @require_superadmin - async def update_service(cls, auth: AuthSchema, id: int, data: TenantUpdateSchema) -> TenantOutSchema: + async def update(self, id: int, data: TenantUpdateSchema) -> TenantOutSchema: """ 更新租户 参数: - - auth (AuthSchema): 认证信息模型 - id (int): 租户ID - data (TenantUpdateSchema): 租户更新模型 返回: - TenantOutSchema: 租户详情 """ - obj = await TenantCRUD(auth).get_or_404(id=id) + obj = await TenantCRUD(self.auth).get_or_404(id=id) old_package_id = obj.package_id @@ -139,19 +139,19 @@ class TenantService: # 套餐变更:仅超管可操作,防止租户管理员自行升级/降级套餐 if data.package_id is not None and data.package_id != old_package_id: - if not auth.user or not auth.user.is_superuser: + if not self.auth.user or not self.auth.user.is_superuser: raise CustomException(msg="仅平台管理员可变更租户套餐") if data.name is not None: - exist = await TenantCRUD(auth).get(name=data.name) + exist = await TenantCRUD(self.auth).get(name=data.name) if exist and exist.id != id: raise CustomException(msg="更新失败,名称重复") if data.code is not None: - exist = await TenantCRUD(auth).get(code=data.code) + exist = await TenantCRUD(self.auth).get(code=data.code) if exist and exist.id != id: raise CustomException(msg="更新失败,编码重复") - updated = await TenantCRUD(auth).update(id=id, data=data) + updated = await TenantCRUD(self.auth).update(id=id, data=data) if not updated: raise CustomException(msg="更新失败") @@ -163,32 +163,33 @@ class TenantService: from app.api.v1.module_platform.package.service import PackageService from app.api.v1.module_system.role.model import RoleMenusModel, RoleModel - available_ids = await PackageService.get_tenant_available_menu_ids(auth, id) + available_ids = await PackageService(self.auth).get_tenant_available_menu_ids(id) if available_ids: role_ids_stmt = select(RoleModel.id).where(RoleModel.tenant_id == id) - result = await auth.db.execute(role_ids_stmt) + result = await self.auth.db.execute(role_ids_stmt) tenant_role_ids = [row[0] for row in result.all()] if tenant_role_ids: - await auth.db.execute( + await self.auth.db.execute( sa_delete(RoleMenusModel).where( RoleMenusModel.role_id.in_(tenant_role_ids), RoleMenusModel.menu_id.notin_(available_ids), ) ) - await auth.db.flush() - logger.info(f"租户[{id}]套餐变更:已清理角色中不再可用的菜单关联, available_menus={len(available_ids)}, roles_affected={len(tenant_role_ids)}") + await self.auth.db.flush() + logger.info( + f"租户[{id}]套餐变更:已清理角色中不再可用的菜单关联, " + f"available_menus={len(available_ids)}, roles_affected={len(tenant_role_ids)}" + ) result = TenantOutSchema.model_validate(updated) return result - @classmethod @require_superadmin - async def delete_service(cls, auth: AuthSchema, ids: list[int]) -> None: + async def delete(self, ids: list[int]) -> None: """ 批量删除租户(含级联资源检查:用户/部门/角色/岗位) 参数: - - auth (AuthSchema): 认证信息模型 - ids (list[int]): 租户ID列表 返回: @@ -205,26 +206,24 @@ class TenantService: for tid in ids: reasons: list[str] = [] - if await UserCRUD(auth).list(search={"tenant_id": tid}): + if await UserCRUD(self.auth).list(search={"tenant_id": tid}): reasons.append("用户") - if await DeptCRUD(auth).list(search={"tenant_id": tid}): + if await DeptCRUD(self.auth).list(search={"tenant_id": tid}): reasons.append("部门") - if await RoleCRUD(auth).list(search={"tenant_id": tid}): + if await RoleCRUD(self.auth).list(search={"tenant_id": tid}): reasons.append("角色") - if await PositionCRUD(auth).list(search={"tenant_id": tid}): + if await PositionCRUD(self.auth).list(search={"tenant_id": tid}): reasons.append("岗位") if reasons: raise CustomException(msg=f"租户下已存在{'/'.join(reasons)},操作失败") - await TenantCRUD(auth).delete(ids=ids) + await TenantCRUD(self.auth).delete(ids=ids) - @classmethod - async def set_available_service(cls, auth: AuthSchema, data: BatchSetAvailable) -> None: + async def set_available(self, data: BatchSetAvailable) -> None: """ 批量设置租户状态 参数: - - auth (AuthSchema): 认证信息模型 - data (BatchSetAvailable): 批量状态设置 返回: @@ -232,28 +231,25 @@ class TenantService: """ if data.status == 1 and 1 in data.ids: raise CustomException(msg="系统租户不允许禁用") - await TenantCRUD(auth).set(ids=data.ids, status=data.status) + await TenantCRUD(self.auth).set(ids=data.ids, status=data.status) - @classmethod - async def toggle_status_service(cls, auth: AuthSchema, id: int) -> None: + async def toggle_status(self, id: int) -> None: """ 切换单个租户的启用/禁用状态 参数: - - auth (AuthSchema): 认证信息模型 - id (int): 租户ID 返回: - None """ - obj = await TenantCRUD(auth).get_or_404(id=id) + obj = await TenantCRUD(self.auth).get_or_404(id=id) if id == 1: raise CustomException(msg="系统租户不允许禁用") new_status = 0 if obj.status == 1 else 1 - await TenantCRUD(auth).set(ids=[id], status=new_status) + await TenantCRUD(self.auth).set(ids=[id], status=new_status) - @classmethod - async def get_tenant_users_service(cls, auth: AuthSchema, tenant_id: int) -> list[TenantUserOutSchema]: + async def get_tenant_users(self, tenant_id: int) -> list[TenantUserOutSchema]: """获取租户下的用户列表""" from sqlalchemy import select @@ -265,7 +261,7 @@ class TenantService: .where(TenantUserModel.tenant_id == tenant_id) .order_by(TenantUserModel.is_default.desc(), TenantUserModel.id) ) - result = await auth.db.execute(stmt) + result = await self.auth.db.execute(stmt) rows = result.all() users = [] @@ -284,13 +280,11 @@ class TenantService: ) return users - @classmethod - async def add_tenant_user_service(cls, auth: AuthSchema, tenant_id: int, data: TenantUserAddSchema) -> None: + async def add_tenant_user(self, tenant_id: int, data: TenantUserAddSchema) -> None: """ 向租户添加用户 参数: - - auth (AuthSchema): 认证信息模型 - tenant_id (int): 租户ID - data (TenantUserAddSchema): 用户添加参数 @@ -298,14 +292,14 @@ class TenantService: - None """ # 验证租户存在 - tenant = await TenantCRUD(auth).get(id=tenant_id) + tenant = await TenantCRUD(self.auth).get(id=tenant_id) if not tenant: raise CustomException(msg="该数据不存在") # 验证用户存在 from app.api.v1.module_system.user.crud import UserCRUD - user = await UserCRUD(auth).get(id=data.user_id) + user = await UserCRUD(self.auth).get(id=data.user_id) if not user: raise CustomException(msg="该数据不存在") @@ -320,16 +314,20 @@ class TenantService: ) .limit(1) ) - result = await auth.db.execute(exist_stmt) + result = await self.auth.db.execute(exist_stmt) if result.scalar_one_or_none(): raise CustomException(msg="该用户已关联此租户") # 如果设为默认租户,先取消其他默认 if data.is_default == 1: - await auth.db.execute(sa.update(TenantUserModel).where(TenantUserModel.user_id == data.user_id).values(is_default=0)) + await self.auth.db.execute( + sa.update(TenantUserModel).where(TenantUserModel.user_id == data.user_id).values(is_default=0) + ) elif data.is_default == 0: # 检查是否是该用户的第一个租户关联 - count_result = await auth.db.execute(select(sa.func.count()).select_from(TenantUserModel).where(TenantUserModel.user_id == data.user_id)) + count_result = await self.auth.db.execute( + select(sa.func.count()).select_from(TenantUserModel).where(TenantUserModel.user_id == data.user_id) + ) count = count_result.scalar() if count == 0: # 第一个租户自动设为默认 @@ -344,18 +342,16 @@ class TenantService: is_default=data.is_default, create_time=datetime.now(), ) - auth.db.add(tu) - await auth.db.flush() + self.auth.db.add(tu) + await self.auth.db.flush() logger.info(f"向租户[{tenant.name}]添加用户[{user.username}]成功, role={data.role}") - @classmethod - async def remove_tenant_user_service(cls, auth: AuthSchema, tenant_id: int, user_id: int) -> None: + async def remove_tenant_user(self, tenant_id: int, user_id: int) -> None: """ 从租户移除用户 参数: - - auth (AuthSchema): 认证信息模型 - tenant_id (int): 租户ID - user_id (int): 用户ID @@ -373,14 +369,14 @@ class TenantService: ) .limit(1) ) - result = await auth.db.execute(exist_stmt) + result = await self.auth.db.execute(exist_stmt) tu = result.scalar_one_or_none() if not tu: raise CustomException(msg="该用户未关联此租户") # 不允许移除租户最后一个 owner if tu.role == "owner": - count_result = await auth.db.execute( + count_result = await self.auth.db.execute( select(sa.func.count()) .select_from(TenantUserModel) .where( @@ -392,22 +388,16 @@ class TenantService: if owner_count <= 1: raise CustomException(msg="租户至少需要保留一个拥有者(owner)") - await auth.db.delete(tu) - await auth.db.flush() + await self.auth.db.delete(tu) + await self.auth.db.flush() logger.info(f"从租户[{tenant_id}]移除用户[{user_id}]成功") - # ============ P1: 配额管理 ============ - # 配额(max_users / max_roles / max_depts / max_storage_mb)由关联的 Package 控制 - # 系统租户 (id=1) 不受配额限制,普通租户从 package_id 对应的套餐读取配额 - - @classmethod - async def get_quota_service(cls, auth: AuthSchema, tenant_id: int) -> dict: + async def get_quota(self, tenant_id: int) -> dict: """ 获取租户配额(从关联套餐读取,系统租户返回无限配额) 参数: - - auth (AuthSchema): 认证信息模型 - tenant_id (int): 租户ID 返回: @@ -422,7 +412,7 @@ class TenantService: "max_depts": 999999, "package_name": "系统租户(无限)", } - tenant = await TenantCRUD(auth).get(id=tenant_id) + tenant = await TenantCRUD(self.auth).get(id=tenant_id) if not tenant: raise CustomException(msg="该数据不存在") if not tenant.package_id: @@ -436,7 +426,7 @@ class TenantService: } from app.api.v1.module_platform.package.crud import PackageCRUD - pkg = await PackageCRUD(auth).get(id=tenant.package_id) + pkg = await PackageCRUD(self.auth).get(id=tenant.package_id) if not pkg: return { "tenant_id": tenant.id, @@ -455,25 +445,19 @@ class TenantService: "package_name": pkg.name, } - @classmethod - async def check_quota_service( - cls, - auth: AuthSchema, - tenant_id: int, - resource_type: str, - ) -> None: + async def check_quota(self, tenant_id: int, resource_type: str) -> None: """检查租户配额是否充足,不足时抛出异常(系统租户跳过检查)""" if tenant_id == 1: return from sqlalchemy import func, select - tenant = await TenantCRUD(auth).get(id=tenant_id) + tenant = await TenantCRUD(self.auth).get(id=tenant_id) if not tenant or not tenant.package_id: return from app.api.v1.module_platform.package.crud import PackageCRUD - pkg = await PackageCRUD(auth).get(id=tenant.package_id) + pkg = await PackageCRUD(self.auth).get(id=tenant.package_id) if not pkg: return @@ -528,43 +512,44 @@ class TenantService: # storage 的实际容量校验在文件上传时进行,此处仅检查是否有配额 return - result = await auth.db.execute(count_stmt) + result = await self.auth.db.execute(count_stmt) current_count = result.scalar() or 0 if current_count >= max_limit: resource_labels = {"user": "用户", "role": "角色", "dept": "部门"} - raise CustomException(msg=f"租户{resource_labels.get(resource_type, resource_type)}数量已达套餐上限({max_limit}),无法继续创建") + raise CustomException( + msg=f"租户{resource_labels.get(resource_type, resource_type)}数量已达套餐上限({max_limit}),无法继续创建" + ) - # ============ P1: 租户配置(已合并到主表) ============ - - @classmethod - async def get_config_service(cls, auth: AuthSchema, tenant_id: int) -> dict: + async def get_config(self, tenant_id: int) -> dict: """ 获取租户所有配置(从租户主表读取,返回原始 dict 供内部使用) 参数: - - auth (AuthSchema): 认证信息模型 - tenant_id (int): 租户ID 返回: - dict: 配置字典 """ - tenant = await TenantCRUD(auth).get(id=tenant_id) + tenant = await TenantCRUD(self.auth).get(id=tenant_id) if not tenant: raise CustomException(msg="该数据不存在") - config_fields = ["name", "description", "version", "logo_url", "favicon", "login_bg", "copyright", "keep_record", "help_doc", "privacy", "clause", "git_code"] + config_fields = [ + "name", "description", "version", "logo_url", "favicon", + "login_bg", "copyright", "keep_record", "help_doc", "privacy", + "clause", "git_code", + ] config = {field: getattr(tenant, field, None) for field in config_fields} return config - @classmethod - async def get_config_items_service(cls, auth: AuthSchema, tenant_id: int) -> list[TenantConfigOutSchema]: + async def get_config_items(self, tenant_id: int) -> list[TenantConfigOutSchema]: """获取租户所有配置(对外接口,返回结构化列表)""" - config = await cls.get_config_service(auth, tenant_id) + config = await self.get_config(tenant_id) return [TenantConfigOutSchema(config_key=k, config_value=str(v) if v is not None else None) for k, v in config.items()] - @classmethod - async def get_config_cache_service(cls, redis: Redis, tenant_id: int) -> dict: + @staticmethod + async def get_config_cache(redis: Redis, tenant_id: int) -> dict: """ 从 Redis 缓存获取租户配置,缓存未命中则从 DB 加载并回写缓存 @@ -589,41 +574,40 @@ class TenantService: async with async_db_session() as session: async with session.begin(): - from app.core.base_schema import AuthSchema + from app.core.base_schema import AuthSchema as _AuthSchema - auth = AuthSchema(db=session, check_data_scope=False) - config = await cls.get_config_service(auth, tenant_id) - await cls._sync_configs_to_redis(redis, tenant_id, config) + _auth = _AuthSchema(db=session, check_data_scope=False) + svc = TenantService(_auth) + config = await svc.get_config(tenant_id) + await TenantService._sync_configs_to_redis(redis, tenant_id, config) logger.info("✅ 已从数据库加载租户配置到缓存") return config - @classmethod - async def get_config_cache_items_service(cls, redis: Redis, tenant_id: int) -> list[TenantConfigOutSchema]: + @staticmethod + async def get_config_cache_items(redis: Redis, tenant_id: int) -> list[TenantConfigOutSchema]: """获取租户缓存配置(对外接口,返回结构化列表)""" - config = await cls.get_config_cache_service(redis, tenant_id) + config = await TenantService.get_config_cache(redis, tenant_id) return [TenantConfigOutSchema(config_key=k, config_value=str(v) if v is not None else None) for k, v in config.items()] - @classmethod - async def _sync_configs_to_redis(cls, redis: Redis, tenant_id: int, config: dict) -> None: + @staticmethod + async def _sync_configs_to_redis(redis: Redis, tenant_id: int, config: dict) -> None: """将租户配置写入 Redis 缓存""" redis_key = f"{RedisInitKeyConfig.TENANT_CONFIG.key}:{tenant_id}" value = json.dumps(config, ensure_ascii=False) await RedisCURD(redis).set(key=redis_key, value=value, expire=None) - @classmethod - async def _del_configs_from_redis(cls, redis: Redis, tenant_id: int) -> None: + @staticmethod + async def _del_configs_from_redis(redis: Redis, tenant_id: int) -> None: """删除租户配置的 Redis 缓存""" redis_key = f"{RedisInitKeyConfig.TENANT_CONFIG.key}:{tenant_id}" await RedisCURD(redis).delete(redis_key) - @classmethod - async def update_config_service(cls, auth: AuthSchema, redis: Redis, tenant_id: int, config: dict) -> list[TenantConfigOutSchema]: + async def update_config(self, redis: Redis, tenant_id: int, config: dict) -> list[TenantConfigOutSchema]: """ 更新租户配置(同步 Redis 缓存) 参数: - - auth (AuthSchema): 认证信息模型 - redis (Redis): Redis 客户端 - tenant_id (int): 租户ID - config (dict): 配置字典 @@ -631,33 +615,30 @@ class TenantService: 返回: - list[TenantConfigOutSchema]: 更新后的配置项列表 """ - tenant = await TenantCRUD(auth).get(id=tenant_id) + tenant = await TenantCRUD(self.auth).get(id=tenant_id) if not tenant: raise CustomException(msg="该数据不存在") - config_fields = ["name", "description", "version", "logo_url", "favicon", "login_bg", "copyright", "keep_record", "help_doc", "privacy", "clause", "git_code"] + config_fields = [ + "name", "description", "version", "logo_url", "favicon", + "login_bg", "copyright", "keep_record", "help_doc", "privacy", + "clause", "git_code", + ] for field in config_fields: if field in config: setattr(tenant, field, config[field]) - await auth.db.flush() + await self.auth.db.flush() # 刷新 DB 数据并同步到 Redis - new_config = await cls.get_config_service(auth, tenant_id) - await cls._sync_configs_to_redis(redis, tenant_id, new_config) + new_config = await self.get_config(tenant_id) + await TenantService._sync_configs_to_redis(redis, tenant_id, new_config) logger.info(f"租户[{tenant_id}]配置已更新") return [TenantConfigOutSchema(config_key=k, config_value=str(v) if v is not None else None) for k, v in new_config.items()] - # ============ P1: 租户菜单 ============ - # 租户菜单权限现通过 Package → PackageMenuModel 统一控制 - # get_menus_service / set_menus_service / get_tenant_menu_ids 已移除 - # 改用 PackageService.get_package_menu_ids / get_tenant_available_menu_ids - - # ============ P1: 初始化缓存 ============ - - @classmethod - async def init_tenant_config_cache(cls, redis: Redis) -> None: + @staticmethod + async def init_cache(redis: Redis) -> None: """ 初始化所有租户配置到 Redis 缓存(应用启动时调用)。 @@ -678,23 +659,23 @@ class TenantService: tenants = result.scalars().all() for tenant in tenants: - config_fields = ["name", "description", "version", "logo_url", "favicon", "login_bg", "copyright", "keep_record", "help_doc", "privacy", "clause", "git_code"] + config_fields = [ + "name", "description", "version", "logo_url", "favicon", + "login_bg", "copyright", "keep_record", "help_doc", "privacy", + "clause", "git_code", + ] config = {field: getattr(tenant, field, None) for field in config_fields} - await cls._sync_configs_to_redis(redis, tenant.id, config) + await TenantService._sync_configs_to_redis(redis, tenant.id, config) logger.info(f"✅ 租户[{tenant.name}](id={tenant.id}) 配置已缓存到 Redis") - # ============ 续期 & 套餐变更预览 ============ - - @classmethod - async def renew_service(cls, auth: AuthSchema, tenant_id: int, end_time: str) -> TenantOutSchema: + async def renew(self, tenant_id: int, end_time: str) -> TenantOutSchema: """租户续期:延长 end_time 并恢复为 active 状态 仅 active(0)/grace(1)/suspended(2) 状态可续期。 expired(4)/frozen(3)/archived(5) 不可续期。 参数: - - auth (AuthSchema): 认证信息 - tenant_id (int): 租户ID - end_time (str): 新的结束时间 @@ -703,7 +684,7 @@ class TenantService: """ from datetime import datetime - tenant = await TenantCRUD(auth).get(id=tenant_id) + tenant = await TenantCRUD(self.auth).get(id=tenant_id) if not tenant: raise CustomException(msg="该数据不存在") @@ -720,20 +701,18 @@ class TenantService: tenant.status = 0 tenant.grace_start_time = None - await auth.db.flush() + await self.auth.db.flush() logger.info(f"租户[{tenant.name}]续期成功, 新的结束时间: {end_time}") return TenantOutSchema.model_validate(tenant) - @classmethod - async def package_change_preview_service(cls, auth: AuthSchema, tenant_id: int, new_package_id: int) -> PackageChangePreviewOut: + async def package_change_preview(self, tenant_id: int, new_package_id: int) -> PackageChangePreviewOut: """ 套餐变更影响预览 返回受影响角色、菜单清单、配额对比等,供超管确认后再执行变更。 参数: - - auth (AuthSchema): 认证信息 - tenant_id (int): 租户ID - new_package_id (int): 目标套餐ID @@ -748,19 +727,19 @@ class TenantService: from app.api.v1.module_system.role.model import RoleMenusModel, RoleModel from app.api.v1.module_system.user.model import UserModel - tenant = await TenantCRUD(auth).get(id=tenant_id) + tenant = await TenantCRUD(self.auth).get(id=tenant_id) if not tenant: raise CustomException(msg="该数据不存在") - new_package = await PackageCRUD(auth).get(id=new_package_id) + new_package = await PackageCRUD(self.auth).get(id=new_package_id) if not new_package: raise CustomException(msg="该数据不存在") # 当前可用菜单 - current_menu_ids = await PackageService.get_tenant_available_menu_ids(auth, tenant_id) + current_menu_ids = await PackageService(self.auth).get_tenant_available_menu_ids(tenant_id) # 新套餐可用菜单(直接取套餐菜单,不再包含自定义授权) - new_menu_ids = set(await PackageService.get_package_menu_ids(auth, new_package_id)) + new_menu_ids = set(await PackageService(self.auth).get_package_menu_ids(new_package_id)) final_menu_ids = new_menu_ids # 不再合并租户自定义菜单 # 差异计算 @@ -771,16 +750,16 @@ class TenantService: added_menus = [] if removed_ids: menu_stmt = select(MenuModel).where(MenuModel.id.in_(removed_ids)) - menu_result = await auth.db.execute(menu_stmt) + menu_result = await self.auth.db.execute(menu_stmt) removed_menus = [{"id": m.id, "name": m.name, "route_path": m.route_path} for m in menu_result.scalars().all()] if added_ids: menu_stmt = select(MenuModel).where(MenuModel.id.in_(added_ids)) - menu_result = await auth.db.execute(menu_stmt) + menu_result = await self.auth.db.execute(menu_stmt) added_menus = [{"id": m.id, "name": m.name, "route_path": m.route_path} for m in menu_result.scalars().all()] # 受影响角色 role_stmt = select(RoleModel).where(RoleModel.tenant_id == tenant_id) - role_result = await auth.db.execute(role_stmt) + role_result = await self.auth.db.execute(role_stmt) roles = role_result.scalars().all() affected_roles = [] @@ -788,13 +767,13 @@ class TenantService: for role in roles: # 查该角色下有多少菜单会被移除 role_menu_stmt = select(RoleMenusModel.menu_id).where(RoleMenusModel.role_id == role.id) - rm_result = await auth.db.execute(role_menu_stmt) + rm_result = await self.auth.db.execute(role_menu_stmt) role_menu_ids = {row[0] for row in rm_result.all()} affected_menu_count = len(role_menu_ids & removed_ids) # 查该角色下用户数 user_count_stmt = select(func.count()).select_from(UserModel).join(UserModel.roles).where(RoleModel.id == role.id) - uc_result = await auth.db.execute(user_count_stmt) + uc_result = await self.auth.db.execute(user_count_stmt) user_count = uc_result.scalar() or 0 affected_roles.append( @@ -811,7 +790,7 @@ class TenantService: # 配额对比(从套餐读取) old_pkg = None if tenant.package_id: - old_pkg = await PackageCRUD(auth).get(id=tenant.package_id) + old_pkg = await PackageCRUD(self.auth).get(id=tenant.package_id) quota_changes = { "max_users": { "current": old_pkg.max_users if old_pkg else 0, @@ -837,8 +816,6 @@ class TenantService: total_affected_users=total_affected_users, ) - # ============ P1: 到期提醒(多阶段自动处理) ============ - @staticmethod async def check_tenant_expiry() -> None: """定时任务:多阶段租户到期自动处理 @@ -860,7 +837,10 @@ class TenantService: async with async_db_session() as session: # 获取所有已过期的活跃租户(status=0) rows = await session.execute( - text("SELECT id, name, contact_email, contact_name, end_time, status FROM platform_tenant WHERE status = '0' AND end_time IS NOT NULL AND end_time < :now"), + text( + "SELECT id, name, contact_email, contact_name, end_time, status " + "FROM platform_tenant WHERE status = '0' AND end_time IS NOT NULL AND end_time < :now" + ), {"now": now}, ) expired_tenants = rows.fetchall() @@ -889,7 +869,9 @@ class TenantService: # 发送通知邮件 if email: - await TenantService._send_expiry_email(tenant_name, contact_name, email, end_time, days_past, label) + await TenantService._send_expiry_email( + tenant_name, contact_name, email, end_time, days_past, label + ) await session.commit() @@ -905,7 +887,12 @@ class TenantService: from app.core.database import async_db_session async with async_db_session() as session: - rows = await session.execute(text("SELECT id, name, contact_email, contact_name, end_time FROM platform_tenant WHERE status = '1' AND contact_email IS NOT NULL AND contact_email != ''")) + rows = await session.execute( + text( + "SELECT id, name, contact_email, contact_name, end_time FROM platform_tenant " + "WHERE status = '1' AND contact_email IS NOT NULL AND contact_email != ''" + ) + ) grace_tenants = rows.fetchall() sent = 0 diff --git a/backend/app/api/v1/module_system/auth/controller.py b/backend/app/api/v1/module_system/auth/controller.py index 83b6b293..459e1b3b 100644 --- a/backend/app/api/v1/module_system/auth/controller.py +++ b/backend/app/api/v1/module_system/auth/controller.py @@ -54,7 +54,7 @@ from .service import ( TenantRegisterService, ) -AuthRouter = APIRouter(route_class=OperationLogRoute, prefix="/auth", tags=["认证授权"]) +AuthRouter = APIRouter(route_class=OperationLogRoute, prefix="/auth", tags=["系统管理", "认证授权"]) _AUTH_TENANTS_NS = "auth_tenants" @@ -70,26 +70,10 @@ async def login_for_access_token_controller( login_form: Annotated[CustomOAuth2PasswordRequestForm, Depends()], db: Annotated[AsyncSession, Depends(db_getter)], ) -> JSONResponse | dict: - """ - 用户登录 - - 参数: - - request (Request): FastAPI请求对象 - - redis (Redis): Redis 客户端对象 - - login_form (CustomOAuth2PasswordRequestForm): 登录表单数据 - - db (AsyncSession): 数据库会话对象 - - 返回: - - LoginWithTenantsSchema: 包含令牌、租户列表和用户信息的响应模型 - - 异常: - - CustomException: 认证失败时抛出异常。 - """ - login_result = await LoginService.authenticate_user_service(request=request, redis=redis, login_form=login_form, db=db) + login_result = await LoginService.authenticate_user(request=request, redis=redis, login_form=login_form, db=db) logger.info(f"用户{login_form.username}登录成功") - # 如果是文档请求,则不记录日志 if settings.DOCS_URL in request.headers.get("referer", ""): return login_result return SuccessResponse(data=login_result, msg="登录成功") @@ -105,21 +89,7 @@ async def get_new_token_controller( db: Annotated[AsyncSession, Depends(db_getter)], redis: Annotated[Redis, Depends(redis_getter)], ) -> JSONResponse: - """ - 刷新token - - 参数: - - payload (RefreshTokenPayloadSchema): 刷新令牌负载模型 - - db (AsyncSession): 数据库会话对象 - - redis (Redis): Redis 客户端对象 - - 返回: - - JWTOutSchema: 包含新的访问令牌和刷新令牌的响应模型 - - 异常: - - CustomException: 刷新令牌失败时抛出异常。 - """ - new_token = await LoginService.refresh_token_service(db=db, redis=redis, refresh_token=payload) + new_token = await LoginService.refresh_token(db=db, redis=redis, refresh_token=payload) return SuccessResponse(data=new_token, msg="刷新成功") @@ -131,20 +101,7 @@ async def get_new_token_controller( async def get_captcha_for_login_controller( redis: Annotated[Redis, Depends(redis_getter)], ) -> JSONResponse: - """ - 获取登录验证码 - - 参数: - - redis (Redis): Redis客户端对象 - - 返回: - - CaptchaOutSchema: 包含验证码图片和key的响应模型 - - 异常: - - CustomException: 获取验证码失败时抛出异常。 - """ - # 获取验证码 - captcha = await CaptchaService.get_captcha_service(redis=redis) + captcha = await CaptchaService.get_captcha(redis=redis) return SuccessResponse(data=captcha, msg="获取验证码成功") @@ -158,20 +115,7 @@ async def logout_controller( payload: LogoutPayloadSchema, redis: Annotated[Redis, Depends(redis_getter)], ) -> JSONResponse: - """ - 退出登录 - - 参数: - - payload (LogoutPayloadSchema): 退出登录负载模型 - - redis (Redis): Redis客户端对象 - - 返回: - - JSONResponse: 包含退出登录结果的响应模型 - - 异常: - - CustomException: 退出登录失败时抛出异常。 - """ - if await LoginService.logout_service(redis=redis, token=payload): + if await LoginService.logout(redis=redis, token=payload): logger.info("退出成功") return SuccessResponse(msg="退出成功") return ErrorResponse(msg="退出失败") @@ -186,18 +130,8 @@ async def get_auto_login_users_controller( auth: Annotated[AuthSchema, Depends(get_current_user)], db: Annotated[AsyncSession, Depends(db_getter)], ) -> JSONResponse: - """ - 获取免登录用户列表 - - 参数: - - auth (AuthSchema): 认证信息 - - db (AsyncSession): 数据库会话对象 - - 返回: - - list[AutoLoginUserSchema]: 免登录用户列表 - """ tenant_id = None if auth.user.is_superuser else auth.user.tenant_id - users = await AutoLoginService.get_auto_login_users_service(db=db, tenant_id=tenant_id) + users = await AutoLoginService.get_auto_login_users(db=db, tenant_id=tenant_id) return SuccessResponse(data=users, msg="获取成功") @@ -212,20 +146,8 @@ async def get_auto_login_token_controller( db: Annotated[AsyncSession, Depends(db_getter)], user_id: int, ) -> JSONResponse: - """ - 获取免登录Token - - 参数: - - auth (AuthSchema): 认证信息 - - redis (Redis): Redis客户端对象 - - db (AsyncSession): 数据库会话对象 - - user_id (int): 用户ID - - 返回: - - AutoLoginTokenSchema: 免登录Token和用户信息 - """ tenant_id = None if auth.user.is_superuser else auth.user.tenant_id - result = await AutoLoginService.create_auto_login_token_service(redis=redis, db=db, user_id=user_id, tenant_id=tenant_id) + result = await AutoLoginService.create_auto_login_token(redis=redis, db=db, user_id=user_id, tenant_id=tenant_id) return SuccessResponse(data=result, msg="获取成功") @@ -240,19 +162,7 @@ async def auto_login_controller( db: Annotated[AsyncSession, Depends(db_getter)], token: str, ) -> JSONResponse: - """ - 免登录 - - 参数: - - request (Request): FastAPI请求对象 - - redis (Redis): Redis客户端对象 - - db (AsyncSession): 数据库会话对象 - - token (str): 免登录Token - - 返回: - - JWTOutSchema: JWT令牌信息 - """ - login_token = await AutoLoginService.auto_login_service(request=request, redis=redis, db=db, token=token) + login_token = await AutoLoginService.auto_login(request=request, redis=redis, db=db, token=token) logger.info("用户免登录成功") return SuccessResponse(data=login_token, msg="登录成功") @@ -269,21 +179,7 @@ async def select_tenant_controller( auth: Annotated[AuthSchema, Depends(get_current_user)], redis: Annotated[Redis, Depends(redis_getter)], ) -> JSONResponse: - """ - 选择租户 - - 验证用户是否属于该租户,签发包含 tenant_id 的新 JWT Token。 - - 参数: - - request (Request): FastAPI请求对象 - - data (SelectTenantSchema): 租户选择请求 - - auth (AuthSchema): 当前认证信息 - - redis (Redis): Redis客户端对象 - - 返回: - - SelectTenantOutSchema: 包含新令牌的响应 - """ - result = await LoginService.select_tenant_service(request=request, redis=redis, auth=auth, tenant_id=data.tenant_id) + result = await LoginService(auth).select_tenant(request=request, redis=redis, tenant_id=data.tenant_id) await FastAPICache.clear(namespace=_AUTH_TENANTS_NS) return SuccessResponse(data=result, msg="租户切换成功") @@ -299,17 +195,8 @@ async def get_user_tenants_controller( auth: Annotated[AuthSchema, Depends(get_current_user)], db: Annotated[AsyncSession, Depends(db_getter)], ) -> JSONResponse: - """ - 获取当前用户的租户列表 - - 参数: - - auth (AuthSchema): 当前认证信息 - - db (AsyncSession): 数据库会话对象 - - 返回: - - list[TenantOptionSchema]: 租户选项列表 - """ - tenants = await LoginService.get_user_tenants_service(auth=auth, db=db) + service = LoginService(auth) + tenants = await service.get_user_tenants() return SuccessResponse(data=tenants, msg="获取租户列表成功") @@ -416,16 +303,7 @@ async def tenant_register_controller( data: TenantRegisterSchema, db: Annotated[AsyncSession, Depends(db_getter)], ) -> JSONResponse: - """ - 租户自助注册(PRD §4.5) - - 参数: - - data (TenantRegisterSchema): 注册信息 - - 返回: - - TenantRegisterOutSchema: 注册结果,含 tenant_id/user_id/试用到期日 - """ - result = await TenantRegisterService.register_service( + result = await TenantRegisterService.register( db=db, username=data.username, password=data.password, @@ -436,9 +314,6 @@ async def tenant_register_controller( return SuccessResponse(data=result, msg=result.message) -# ─── 忘记密码(自助重置)──────────────────────────────────── - - @AuthRouter.post( "/forgot-password", summary="忘记密码", @@ -449,16 +324,7 @@ async def forgot_password_controller( redis: Annotated[Redis, Depends(redis_getter)], db: Annotated[AsyncSession, Depends(db_getter)], ) -> JSONResponse: - """ - 忘记密码:提交邮箱,接收重置邮件 - - 参数: - - data (ForgotPasswordSchema): 邮箱 - - 返回: - - str: 提示信息(无论成功与否均返回相同文案) - """ - msg = await PasswordResetService.forgot_password_service(redis=redis, db=db, email=data.email) + msg = await PasswordResetService.forgot_password(redis=redis, db=db, email=data.email) return SuccessResponse(data=msg, msg="邮件已发送") @@ -472,14 +338,5 @@ async def reset_password_controller( redis: Annotated[Redis, Depends(redis_getter)], db: Annotated[AsyncSession, Depends(db_getter)], ) -> JSONResponse: - """ - 重置密码:使用令牌 + 新密码 - - 参数: - - data (ResetPasswordWithTokenSchema): 令牌 + 新密码 - - 返回: - - str: 重置结果 - """ - msg = await PasswordResetService.reset_password_with_token_service(redis=redis, db=db, token=data.token, new_password=data.new_password) + msg = await PasswordResetService.reset_password_with_token(redis=redis, db=db, token=data.token, new_password=data.new_password) return SuccessResponse(data=msg, msg="密码已重置") diff --git a/backend/app/api/v1/module_system/auth/oauth_service.py b/backend/app/api/v1/module_system/auth/oauth_service.py index 8dea5b97..afd93e23 100644 --- a/backend/app/api/v1/module_system/auth/oauth_service.py +++ b/backend/app/api/v1/module_system/auth/oauth_service.py @@ -323,7 +323,7 @@ async def ensure_oauth_user( role_ids=list(settings.OAUTH_DEFAULT_ROLE_IDS), ) try: - await UserService.register_service(auth=auth, data=reg) + await UserService(auth).register(data=reg) except Exception: # 并发创建可能触发唯一约束冲突,回退到再次查询 existing = await UserCRUD(auth).get(username=username) @@ -389,7 +389,7 @@ async def complete_oauth_login( raise CustomException(msg="用户不存在") login_type = f"oauth_{provider}" - token = await LoginService.create_token_service(request=request, redis=redis, user=user, login_type=login_type) + token = await LoginService.create_token(request=request, redis=redis, user=user, login_type=login_type) await rc.delete(f"{STATE_PREFIX}{state}") return token, frontend diff --git a/backend/app/api/v1/module_system/auth/schema.py b/backend/app/api/v1/module_system/auth/schema.py index 49e0e788..b9a98e62 100644 --- a/backend/app/api/v1/module_system/auth/schema.py +++ b/backend/app/api/v1/module_system/auth/schema.py @@ -55,7 +55,7 @@ class SelectTenantOutSchema(BaseModel): model_config = ConfigDict(from_attributes=True) access_token: str = Field(..., description="访问token(含租户上下文)") - token_type: str = Field(default="Bearer", description="token类型") + token_type: str = Field(default="Bearer", description="token类型(RFC 6750)") expires_in: int = Field(..., gt=0, description="过期时间(秒)") @@ -66,9 +66,6 @@ class LoginWithTenantsSchema(JWTOutSchema): user_info: dict = Field(default_factory=dict, description="用户信息") -# ─── 租户自助注册 ────────────────────────────────────────────────────────────── - - class TenantRegisterSchema(BaseModel): """租户自助注册请求""" @@ -91,9 +88,6 @@ class TenantRegisterOutSchema(BaseModel): message: str = Field(default="注册成功", description="提示信息") -# ─── 忘记密码(自助重置)───────────────────────────────────────── - - class ForgotPasswordSchema(BaseModel): """忘记密码:提交邮箱,接收重置邮件""" diff --git a/backend/app/api/v1/module_system/auth/service.py b/backend/app/api/v1/module_system/auth/service.py index 74130c23..03c8f6da 100644 --- a/backend/app/api/v1/module_system/auth/service.py +++ b/backend/app/api/v1/module_system/auth/service.py @@ -58,23 +58,24 @@ async def _write_login_log( msg: str | None = None, ) -> None: """写入登录日志(独立 session,避免事务回滚时丢失失败记录)""" - from app.api.v1.module_system.log.model import LoginLogModel + from app.api.v1.module_system.log.crud import LoginLogCRUD + from app.api.v1.module_system.log.schema import LoginLogCreateSchema + from app.core.base_schema import AuthSchema from app.core.database import async_db_session try: async with async_db_session() as session: async with session.begin(): - session.add( - LoginLogModel( - username=username, - status=status, - login_ip=login_ip, - login_location=login_location, - request_os=request_os, - request_browser=request_browser, - msg=msg, - ) - ) + _auth = AuthSchema(db=session, check_data_scope=False) + await LoginLogCRUD(_auth).create(data=LoginLogCreateSchema( + username=username, + status=status, + login_ip=login_ip, + login_location=login_location, + request_os=request_os, + request_browser=request_browser, + msg=msg, + )) except Exception: pass # 登录日志写入失败不影响登录主流程 @@ -90,29 +91,18 @@ def _resolve_request_ip(request: Request) -> str: class LoginService: """登录认证服务""" + def __init__(self, auth: AuthSchema | None = None) -> None: + self.auth = auth + @classmethod - async def authenticate_user_service( + async def authenticate_user( cls, request: Request, redis: Redis, login_form: CustomOAuth2PasswordRequestForm, db: AsyncSession, ) -> LoginWithTenantsSchema: - """ - 用户认证 - - 参数: - - request (Request): FastAPI请求对象 - - login_form (CustomOAuth2PasswordRequestForm): 登录表单数据 - - db (AsyncSession): 数据库会话对象 - - 返回: - - LoginWithTenantsSchema: 包含令牌和租户列表的响应模型 - - 异常: - - CustomException: 认证失败时抛出异常。 - """ - # 解析请求信息(IP、UA),供登录日志使用 + """用户认证""" user_agent = parse(request.headers.get("user-agent")) request_ip = _resolve_request_ip(request) login_location = await IpLocalUtil.resolve_location_for_log(request_ip) @@ -120,21 +110,19 @@ class LoginService: _login_browser = user_agent.browser.family _login_username = login_form.username - # 判断是否来自API文档 referer = request.headers.get("referer", "") request_from_docs = referer.endswith(("docs", "redoc")) - # 验证码校验 if settings.CAPTCHA_ENABLE and not request_from_docs: if not login_form.captcha_key or not login_form.captcha: raise CustomException(msg="验证码不能为空") - await CaptchaService.check_captcha_service( + await CaptchaService.check_captcha( redis=redis, key=login_form.captcha_key, captcha=login_form.captcha, ) - # 用户认证 - auth = AuthSchema(db=db) + + auth = AuthSchema(db=db, check_data_scope=False) user = await UserCRUD(auth).get(username=login_form.username) if not user: @@ -172,7 +160,6 @@ class LoginService: ) raise CustomException(msg="用户已被停用") - # 检查用户的默认租户是否正常 from sqlalchemy import select from app.api.v1.module_platform.tenant.model import TenantModel @@ -191,7 +178,6 @@ class LoginService: ) raise CustomException(msg="所属租户已被禁用,请联系平台管理员") - # 更新最后登录时间 await UserCRUD(auth).update_last_login(id=user.id) if not user: @@ -199,30 +185,24 @@ class LoginService: if not login_form.login_type: raise CustomException(msg="登录类型不能为空") - # 创建token - token = await cls.create_token_service( + token = await cls.create_token( request=request, redis=redis, user=user, login_type=login_form.login_type, ) - # 查询用户关联的租户列表 - tenants = await cls.get_user_tenants_service( - auth=AuthSchema(db=db, user=user, tenant_id=user.tenant_id, check_data_scope=False), - db=db, - user_id=user.id, - ) + tenants_auth = AuthSchema(db=db, user=user, tenant_id=user.tenant_id, check_data_scope=False) + tenants = await LoginService(tenants_auth).get_user_tenants(user_id=user.id) user_info = { "id": user.id, "username": user.username, "name": user.name, "avatar": user.avatar, - "is_super_admin": user.is_superuser, + "is_superuser": user.is_superuser, } - # 写入登录成功日志 await _write_login_log( username=user.username, status=1, @@ -243,46 +223,36 @@ class LoginService: ) @classmethod - async def create_token_service(cls, request: Request, redis: Redis, user: UserModel, login_type: str) -> JWTOutSchema: - """ - 创建访问令牌和刷新令牌 - - 参数: - - request (Request): FastAPI请求对象 - - redis (Redis): Redis客户端对象 - - user (UserModel): 用户模型对象 - - login_type (str): 登录类型 - - 返回: - - JWTOutSchema: 包含访问令牌和刷新令牌的响应模型 - - 异常: - - CustomException: 创建令牌失败时抛出异常。 - """ - # 生成会话编号 + async def create_token(cls, request: Request, redis: Redis, user: UserModel, login_type: str) -> JWTOutSchema: + """创建访问令牌和刷新令牌""" session_id = str(uuid.uuid4()) - request.scope["session_id"] = session_id - user_agent = parse(request.headers.get("user-agent")) request_ip = _resolve_request_ip(request) login_location = await IpLocalUtil.resolve_location_for_log(request_ip) - request.scope["login_location"] = login_location - # 确保在请求上下文中设置用户名和会话ID - request.scope["user_username"] = user.username + from dataclasses import replace + + from app.core.request_context import RequestContext + + base_ctx = getattr(request.state, "ctx", None) or RequestContext() + request.state.ctx = replace( + base_ctx, + session_id=session_id, + user_username=user.username, + login_location=login_location, + ) access_expires = timedelta(seconds=settings.ACCESS_TOKEN_EXPIRE_MINUTES) refresh_expires = timedelta(seconds=settings.REFRESH_TOKEN_EXPIRE_MINUTES) now = datetime.now() - # 生成会话信息 session_info = OnlineOutSchema( session_id=session_id, user_id=user.id, tenant_id=user.tenant_id, - is_super_admin=user.is_superuser, + is_superuser=user.is_superuser, name=user.name, user_name=user.username, ipaddr=request_ip, @@ -308,7 +278,6 @@ class LoginService: ) ) - # 设置新的token await RedisCURD(redis).set( key=f"{RedisInitKeyConfig.ACCESS_TOKEN.key}:{session_id}", value=access_token, @@ -329,31 +298,17 @@ class LoginService: ) @classmethod - async def refresh_token_service( + async def refresh_token( cls, db: AsyncSession, redis: Redis, refresh_token: RefreshTokenPayloadSchema, ) -> JWTOutSchema: - """ - 刷新访问令牌 - - 参数: - - db (AsyncSession): 数据库会话对象 - - redis (Redis): Redis客户端对象 - - refresh_token (RefreshTokenPayloadSchema): 刷新令牌数据 - - 返回: - - JWTOutSchema: 新的令牌对象 - - 异常: - - CustomException: 刷新令牌无效时抛出异常 - """ + """刷新访问令牌""" token_payload: JWTPayloadSchema = decode_access_token(token=refresh_token.refresh_token) if not token_payload.is_refresh: raise CustomException(msg="非法凭证,请传入刷新令牌") - # 去 Redis 查完整信息 session_info = json.loads(token_payload.sub) session_id = session_info.get("session_id") user_id = session_info.get("user_id") @@ -361,15 +316,13 @@ class LoginService: if not session_id or not user_id: raise CustomException(msg="非法凭证,无法获取会话编号或用户ID") - # 用户认证 - auth = AuthSchema(db=db) + auth = AuthSchema(db=db, check_data_scope=False) user = await UserCRUD(auth).get(id=user_id) if not user: raise CustomException(msg="刷新token失败,用户不存在") if user.status == 1: raise CustomException(msg="用户已被停用") - # 设置新的 token access_expires = timedelta(seconds=settings.ACCESS_TOKEN_EXPIRE_MINUTES) refresh_expires = timedelta(seconds=settings.REFRESH_TOKEN_EXPIRE_MINUTES) now = datetime.now() @@ -392,7 +345,6 @@ class LoginService: ) ) - # 覆盖写入 Redis await RedisCURD(redis).set( key=f"{RedisInitKeyConfig.ACCESS_TOKEN.key}:{session_id}", value=access_token, @@ -412,21 +364,9 @@ class LoginService: expires_in=int(access_expires.total_seconds()), ) - @classmethod - async def logout_service(cls, redis: Redis, token: LogoutPayloadSchema) -> bool: - """ - 退出登录 - - 参数: - - redis (Redis): Redis客户端对象 - - token (LogoutPayloadSchema): 退出登录令牌数据 - - 返回: - - bool: 退出成功返回True - - 异常: - - CustomException: 令牌无效时抛出异常 - """ + @staticmethod + async def logout(redis: Redis, token: LogoutPayloadSchema) -> bool: + """退出登录""" payload: JWTPayloadSchema = decode_access_token(token=token.token) session_info = json.loads(payload.sub) session_id = session_info.get("session_id") @@ -434,7 +374,6 @@ class LoginService: if not session_id: raise CustomException(msg="非法凭证,无法获取会话编号") - # 删除Redis中的在线用户、访问令牌、刷新令牌 await RedisCURD(redis).delete(f"{RedisInitKeyConfig.ACCESS_TOKEN.key}:{session_id}") await RedisCURD(redis).delete(f"{RedisInitKeyConfig.REFRESH_TOKEN.key}:{session_id}") @@ -442,40 +381,25 @@ class LoginService: return True - @classmethod - async def get_user_tenants_service( - cls, - auth: AuthSchema, - db: AsyncSession, + async def get_user_tenants( + self, user_id: int | None = None, ) -> list[TenantOptionSchema]: - """ - 获取用户关联的租户列表 - - 参数: - - auth (AuthSchema): 认证信息对象 - - db (AsyncSession): 数据库会话对象 - - user_id (int | None): 用户ID,未传入时从 auth.user 获取 - - 返回: - - list[TenantOptionSchema]: 租户选项列表 - """ + """获取用户关联的租户列表""" from sqlalchemy import select from app.api.v1.module_platform.tenant.model import TenantModel, TenantUserModel - uid = user_id or (auth.user.id if auth.user else None) + uid = user_id or (self.auth.user.id if self.auth.user else None) if not uid: return [] - # 超管可以看到所有租户 - if auth.user and auth.user.is_superuser: + if self.auth.user and self.auth.user.is_superuser: stmt = select(TenantModel).where(TenantModel.status == 0, TenantModel.is_deleted.is_(False)).order_by(TenantModel.sort, TenantModel.id) - result = await db.execute(stmt) + result = await self.auth.db.execute(stmt) tenant_objs = result.scalars().all() return [TenantOptionSchema(id=t.id, name=t.name, code=t.code) for t in tenant_objs] - # 普通用户通过 sys_user_tenant 关联表查询 stmt = ( select(TenantModel) .join(TenantUserModel, TenantUserModel.tenant_id == TenantModel.id) @@ -486,73 +410,52 @@ class LoginService: ) .order_by(TenantUserModel.is_default.desc(), TenantModel.sort, TenantModel.id) ) - result = await db.execute(stmt) + result = await self.auth.db.execute(stmt) tenant_objs = result.scalars().all() return [TenantOptionSchema(id=t.id, name=t.name, code=t.code) for t in tenant_objs] - @classmethod - async def select_tenant_service( - cls, + async def select_tenant( + self, request: Request, redis: Redis, - auth: AuthSchema, tenant_id: int, ) -> SelectTenantOutSchema: - """ - 选择租户:验证用户归属并签发含租户上下文的新 JWT Token - - 参数: - - request (Request): FastAPI请求对象 - - redis (Redis): Redis客户端对象 - - auth (AuthSchema): 当前认证信息 - - tenant_id (int): 目标租户ID - - 返回: - - SelectTenantOutSchema: 包含新令牌的响应 - - 异常: - - CustomException: 用户不属于该租户时抛出 - """ + """选择租户:验证用户归属并签发含租户上下文的新 JWT Token""" from sqlalchemy import select from app.api.v1.module_platform.tenant.model import TenantModel, TenantUserModel - if not auth.user: + if not self.auth.user: raise CustomException(msg="未认证用户") - # 超管可以选择任意租户 - if not auth.user.is_superuser: - # 验证用户是否属于该租户 + if not self.auth.user.is_superuser: exist_stmt = ( select(TenantUserModel) .where( - TenantUserModel.user_id == auth.user.id, + TenantUserModel.user_id == self.auth.user.id, TenantUserModel.tenant_id == tenant_id, ) .limit(1) ) - result = await auth.db.execute(exist_stmt) + result = await self.auth.db.execute(exist_stmt) if not result.scalar_one_or_none(): raise CustomException(msg="您不属于该租户,无法切换") - # 验证租户是否存在且状态正常 tenant_stmt = select(TenantModel).where(TenantModel.id == tenant_id, TenantModel.status == 0).limit(1) - result = await auth.db.execute(tenant_stmt) + result = await self.auth.db.execute(tenant_stmt) tenant = result.scalar_one_or_none() if not tenant: raise CustomException(msg="租户不存在或已被禁用") - # 获取当前会话信息 - session_id = request.scope.get("session_id") - session_info = request.scope.get("session_info") + ctx = getattr(request.state, "ctx", None) + session_id = ctx.session_id if ctx else None + session_info = ctx.session_info if ctx else None if not session_id or not session_info: raise CustomException(msg="会话已失效") - # 更新会话信息中的 tenant_id session_info["tenant_id"] = tenant_id - # 签发新的 access_token(含新的 tenant_id) from app.core.security import create_access_token access_expires = timedelta(seconds=settings.ACCESS_TOKEN_EXPIRE_MINUTES) @@ -566,7 +469,6 @@ class LoginService: ) ) - # 覆盖 Redis 中的 access_token from app.core.redis_crud import RedisCURD await RedisCURD(redis).set( @@ -575,7 +477,6 @@ class LoginService: expire=int(access_expires.total_seconds()), ) - # 同时签发并覆盖 refresh_token(含新 tenant_id)避免刷新时回退 refresh_expires = timedelta(seconds=settings.REFRESH_TOKEN_EXPIRE_MINUTES) new_refresh_token = create_access_token( payload=JWTPayloadSchema( @@ -590,12 +491,11 @@ class LoginService: expire=int(refresh_expires.total_seconds()), ) - # 同时更新租户上下文 - from app.core.tenant import set_current_tenant + from app.core.request_context import set_current_tenant - set_current_tenant(tenant_id, auth.user.is_superuser) + set_current_tenant(tenant_id) - logger.info(f"用户 {auth.user.username}(id={auth.user.id}) 切换到租户 {tenant.name}(id={tenant_id})") + logger.info(f"用户 {self.auth.user.username}(id={self.auth.user.id}) 切换到租户 {tenant.name}(id={tenant_id})") return SelectTenantOutSchema( access_token=new_access_token, @@ -607,28 +507,15 @@ class LoginService: class CaptchaService: """验证码服务""" - @classmethod - async def get_captcha_service(cls, redis: Redis) -> CaptchaOutSchema: - """ - 获取验证码 - - 参数: - - redis (Redis): Redis客户端对象 - - 返回: - - dict[str, CaptchaKey | CaptchaBase64]: 包含验证码key和base64图片的字典 - - 异常: - - CustomException: 验证码服务未启用时抛出异常 - """ + @staticmethod + async def get_captcha(redis: Redis) -> CaptchaOutSchema: + """获取验证码""" if not settings.CAPTCHA_ENABLE: raise CustomException(msg="未开启验证码服务") - # 生成验证码图片和值 captcha_base64, captcha_value = CaptchaUtil.captcha_arithmetic() captcha_key = get_random_character() - # 保存到Redis并设置过期时间 redis_key = f"{RedisInitKeyConfig.CAPTCHA_CODES.key}:{captcha_key}" await RedisCURD(redis).set( key=redis_key, @@ -636,44 +523,26 @@ class CaptchaService: expire=settings.CAPTCHA_EXPIRE_SECONDS, ) - # 返回验证码信息 return CaptchaOutSchema( enable=settings.CAPTCHA_ENABLE, key=CaptchaKey(captcha_key), img_base=CaptchaBase64(f"data:image/png;base64,{captcha_base64}"), ) - @classmethod - async def check_captcha_service(cls, redis: Redis, key: str, captcha: str) -> bool: - """ - 校验验证码 - - 参数: - - redis (Redis): Redis客户端对象 - - key (str): 验证码key - - captcha (str): 用户输入的验证码 - - 返回: - - bool: 验证通过返回True - - 异常: - - CustomException: 验证码无效或错误时抛出异常 - """ + @staticmethod + async def check_captcha(redis: Redis, key: str, captcha: str) -> bool: + """校验验证码""" if not captcha: raise CustomException(msg="验证码不能为空") - # 获取Redis中存储的验证码 redis_key = f"{RedisInitKeyConfig.CAPTCHA_CODES.key}:{key}" - captcha_value = await RedisCURD(redis).get(redis_key) if not captcha_value: raise CustomException(msg="验证码已过期") - # 验证码不区分大小写比对 if captcha.lower() != captcha_value.lower(): raise CustomException(msg="验证码错误") - # 验证成功后删除验证码,避免重复使用 await RedisCURD(redis).delete(redis_key) return True @@ -681,23 +550,12 @@ class CaptchaService: class AutoLoginService: """免登录服务""" - # 免登录Token前缀 AUTO_LOGIN_PREFIX = "fastapiadmin:auto_login:" - # Token有效期(秒) - 5分钟 TOKEN_EXPIRE = 300 @classmethod - async def get_auto_login_users_service(cls, db: AsyncSession, tenant_id: int | None = None) -> list[AutoLoginUserSchema]: - """ - 获取免登录用户列表 - - 参数: - - db (AsyncSession): 数据库会话对象 - - tenant_id (int | None): 租户ID,非超管时必传以限制租户范围 - - 返回: - - list[AutoLoginUserSchema]: 用户列表 - """ + async def get_auto_login_users(cls, db: AsyncSession, tenant_id: int | None = None) -> list[AutoLoginUserSchema]: + """获取免登录用户列表""" from sqlalchemy import select from app.api.v1.module_system.user.model import UserModel @@ -720,29 +578,14 @@ class AutoLoginService: ] @classmethod - async def create_auto_login_token_service( + async def create_auto_login_token( cls, redis: Redis, db: AsyncSession, user_id: int, tenant_id: int | None = None, ) -> AutoLoginTokenSchema: - """ - 创建免登录Token - - 参数: - - request (Request): FastAPI请求对象 - - redis (Redis): Redis客户端对象 - - db (AsyncSession): 数据库会话对象 - - user_id (int): 用户ID - - tenant_id (int | None): 租户ID,非超管时必传以防止跨租户操作 - - 返回: - - AutoLoginTokenSchema: 免登录Token和用户信息 - - 异常: - - CustomException: 用户不存在或已停用时抛出异常 - """ + """创建免登录Token""" from sqlalchemy import select from app.api.v1.module_system.user.model import UserModel @@ -759,13 +602,11 @@ class AutoLoginService: if user.status == 1: raise CustomException(msg="用户已被停用") - # 生成免登录Token import uuid token = str(uuid.uuid4()) token_key = f"{cls.AUTO_LOGIN_PREFIX}{token}" - # 存储到Redis,设置5分钟过期 token_data = { "user_id": user.id, "username": user.username, @@ -791,7 +632,7 @@ class AutoLoginService: ) @classmethod - async def auto_login_service( + async def auto_login( cls, request: Request, redis: Redis, @@ -799,22 +640,7 @@ class AutoLoginService: token: str, tenant_id: int | None = None, ) -> JWTOutSchema: - """ - 免登录 - - 参数: - - request (Request): FastAPI请求对象 - - redis (Redis): Redis客户端对象 - - db (AsyncSession): 数据库会话对象 - - token (str): 免登录Token - - tenant_id (int | None): 租户ID,非超管时必传以防止跨租户登录 - - 返回: - - JWTOutSchema: JWT令牌信息 - - 异常: - - CustomException: Token无效或过期时抛出异常 - """ + """免登录""" from sqlalchemy import select from app.api.v1.module_system.user.model import UserModel @@ -845,27 +671,22 @@ class AutoLoginService: if user.status == 1: raise CustomException(msg="用户已被停用") - # 删除已使用的Token await RedisCURD(redis).delete(token_key) - # 使用LoginService创建token - jwt_token = await LoginService.create_token_service(request=request, redis=redis, user=user, login_type="PC端") + jwt_token = await LoginService.create_token(request=request, redis=redis, user=user, login_type="PC端") logger.info(f"用户{user.username}免登录成功") return jwt_token -# ─── 租户自助注册 (PRD §4.5) ──────────────────────────────── - - class TenantRegisterService: """PRD §4.5 租户自助注册:一次性创建租户 + 管理员 + owner 角色 + 菜单分配""" DEFAULT_TRIAL_DAYS = 7 @classmethod - async def register_service( + async def register( cls, db: AsyncSession, username: str, @@ -873,22 +694,7 @@ class TenantRegisterService: email: str, tenant_name: str | None = None, ) -> TenantRegisterOutSchema: - """ - 租户自助注册:一次性创建租户 + 管理员 + owner 角色 + 菜单分配。 - - 参数: - - db (AsyncSession): 数据库会话对象。 - - username (str): 登录账号。 - - password (str): 登录密码。 - - email (str): 邮箱。 - - tenant_name (str | None): 企业/团队名称。 - - 返回: - - TenantRegisterOutSchema: 注册结果。 - - 异常: - - CustomException: 用户名或邮箱已被占用时抛出。 - """ + """租户自助注册:一次性创建租户 + 管理员 + owner 角色 + 菜单分配""" from sqlalchemy import func, select from sqlalchemy.exc import IntegrityError @@ -897,7 +703,6 @@ class TenantRegisterService: from app.api.v1.module_system.role.model import RoleMenusModel, RoleModel from app.api.v1.module_system.user.model import UserModel, UserRolesModel - # ── 1. 唯一性校验 ── exists_stmt = ( select(func.count()) .select_from(UserModel) @@ -910,20 +715,16 @@ class TenantRegisterService: if cnt > 0: raise CustomException(msg="用户名或邮箱已被占用") - # ── 2. 获取默认套餐 ── pkg_stmt = select(PackageModel).where(PackageModel.status == 0).order_by(PackageModel.id).limit(1) default_pkg = (await db.execute(pkg_stmt)).scalar_one_or_none() - # ── 3. 计算试用期 ── now = datetime.now() trial_end = now + timedelta(days=cls.DEFAULT_TRIAL_DAYS) - # ── 4. 生成租户 code ── base = tenant_name or username code_suffix = base.encode("utf-8").hex()[:6].upper() tenant_code = f"T{code_suffix}" - # ── 5. 创建租户 ── tenant = TenantModel( name=tenant_name or f"{username}的租户", code=tenant_code, @@ -936,7 +737,6 @@ class TenantRegisterService: db.add(tenant) await db.flush() - # ── 6. 创建管理员用户 ── user = UserModel( username=username, password=PwdUtil.hash_password(password), @@ -947,23 +747,20 @@ class TenantRegisterService: db.add(user) await db.flush() - # ── 7. 创建 owner 角色 ── owner_role = RoleModel( name="租户管理员", code="owner", tenant_id=tenant.id, order=1, - data_scope=4, # 全部数据权限 + data_scope=4, description="自助注册创建的管理员角色", ) db.add(owner_role) await db.flush() - # ── 8. 绑定用户角色 ── user_role = UserRolesModel(user_id=user.id, role_id=owner_role.id) db.add(user_role) - # ── 9. 分配套餐菜单 ── if default_pkg: pkg_menu_stmt = select(PackageMenuModel).where( PackageMenuModel.package_id == default_pkg.id, @@ -978,7 +775,6 @@ class TenantRegisterService: await db.rollback() raise CustomException(msg="租户编码或用户名已被占用,请重试") - # ── 10. 发送欢迎邮件 ── try: await cls._send_welcome_email(email, username, tenant.name, trial_end) except Exception: @@ -998,38 +794,23 @@ class TenantRegisterService: @classmethod async def _send_welcome_email(cls, to_email: str, username: str, tenant_name: str, trial_end: datetime) -> None: """发送欢迎邮件(不阻塞注册流程)。""" - from sqlalchemy import select - - from app.api.v1.module_platform.email.model import EmailConfigModel + from app.api.v1.module_platform.email.crud import EmailConfigCRUD + from app.core.base_schema import AuthSchema from app.core.database import async_db_session - from app.utils.email_util import render_template, send_email + from app.utils.email_util import render_template_file, send_email async with async_db_session() as _db: - cfg_stmt = ( - select(EmailConfigModel) - .where( - EmailConfigModel.is_default.is_(True), - EmailConfigModel.status == 0, - ) - .limit(1) - ) - cfg = (await _db.execute(cfg_stmt)).scalar_one_or_none() + cfg = await EmailConfigCRUD(AuthSchema(db=_db, check_data_scope=False)).get_active_default() if not cfg: logger.info("无可用 SMTP 配置,跳过欢迎邮件") return - html_body = render_template( - """

欢迎加入 {{ tenant_name }}!

-

{{ username }},您好!

-

您的租户已成功创建,试用期至 {{ trial_end }}

-

请登录后台开始使用。

""", - { - "tenant_name": tenant_name, - "username": username, - "trial_end": trial_end.strftime("%Y-%m-%d"), - }, - ) + html_body = render_template_file("emails/welcome.jinja2", { + "tenant_name": tenant_name, + "username": username, + "trial_end": trial_end.strftime("%Y-%m-%d"), + }) await send_email( smtp_host=cfg.smtp_host, @@ -1046,9 +827,6 @@ class TenantRegisterService: logger.info(f"欢迎邮件已发送至 {to_email}") -# ─── 忘记密码自助重置 (PRD §4.6) ───────────────────────────── - - class PasswordResetService: """PRD §4.6 忘记密码:邮箱重置令牌 + 密码更新""" @@ -1056,11 +834,8 @@ class PasswordResetService: TOKEN_EXPIRE_SECONDS = 1800 # 30 分钟 @classmethod - async def forgot_password_service(cls, redis: Redis, db: AsyncSession, email: str) -> str: - """ - 忘记密码:根据邮箱查找用户,生成重置令牌并尝试发送邮件。 - 无论邮箱是否存在均返回相同文案(防止邮箱探测攻击)。 - """ + async def forgot_password(cls, redis: Redis, db: AsyncSession, email: str) -> str: + """忘记密码:根据邮箱查找用户,生成重置令牌并尝试发送邮件。""" from sqlalchemy import select from app.api.v1.module_system.user.model import UserModel @@ -1076,12 +851,10 @@ class PasswordResetService: logger.info(f"忘记密码:邮箱 {email} 未注册,静默返回") return "若邮箱已注册,重置邮件已发送" - # 生成一次性令牌 token = secrets.token_urlsafe(32) key = f"{cls.RESET_TOKEN_PREFIX}{token}" await RedisCURD(redis).set(key=key, value=str(user.id), expire=cls.TOKEN_EXPIRE_SECONDS) - # 尝试发送邮件(不阻塞) try: await cls._send_reset_email(email, user.username, token) except Exception: @@ -1090,9 +863,8 @@ class PasswordResetService: return "若邮箱已注册,重置邮件已发送" @classmethod - async def reset_password_with_token_service(cls, redis: Redis, db: AsyncSession, token: str, new_password: str) -> str: + async def reset_password_with_token(cls, redis: Redis, db: AsyncSession, token: str, new_password: str) -> str: """使用令牌重置密码。校验令牌 → 更新密码 → 删除令牌。""" - from app.api.v1.module_system.user.model import UserModel key = f"{cls.RESET_TOKEN_PREFIX}{token}" @@ -1122,40 +894,24 @@ class PasswordResetService: @classmethod async def _send_reset_email(cls, to_email: str, username: str, token: str) -> None: """发送密码重置邮件。""" - from sqlalchemy import select - - from app.api.v1.module_platform.email.model import EmailConfigModel + from app.api.v1.module_platform.email.crud import EmailConfigCRUD + from app.core.base_schema import AuthSchema from app.core.database import async_db_session - from app.utils.email_util import render_template, send_email + from app.utils.email_util import render_template_file, send_email async with async_db_session() as _db: - cfg_stmt = ( - select(EmailConfigModel) - .where( - EmailConfigModel.is_default.is_(True), - EmailConfigModel.status == 0, - ) - .limit(1) - ) - cfg = (await _db.execute(cfg_stmt)).scalar_one_or_none() + cfg = await EmailConfigCRUD(AuthSchema(db=_db, check_data_scope=False)).get_active_default() if not cfg: logger.info("无可用 SMTP 配置,跳过重置邮件") return reset_url = f"{getattr(settings, 'SITE_URL', '')}/reset-password?token={token}" - html_body = render_template( - """

密码重置

-

{{ username }},您好!

-

请点击以下链接重置密码({{ expire_minutes }} 分钟内有效):

-

{{ reset_url }}

-

如果非您本人操作,请忽略此邮件。

""", - { - "username": username, - "reset_url": reset_url, - "expire_minutes": cls.TOKEN_EXPIRE_SECONDS // 60, - }, - ) + html_body = render_template_file("emails/reset_password.jinja2", { + "username": username, + "reset_url": reset_url, + "expire_minutes": cls.TOKEN_EXPIRE_SECONDS // 60, + }) await send_email( smtp_host=cfg.smtp_host, diff --git a/backend/app/api/v1/module_system/dept/controller.py b/backend/app/api/v1/module_system/dept/controller.py index 7a07eba1..6a6490ec 100644 --- a/backend/app/api/v1/module_system/dept/controller.py +++ b/backend/app/api/v1/module_system/dept/controller.py @@ -13,11 +13,10 @@ from app.core.router_class import OperationLogRoute from .schema import DeptCreateSchema, DeptOutSchema, DeptQueryParam, DeptUpdateSchema from .service import DeptService -DeptRouter = APIRouter(route_class=OperationLogRoute, prefix="/dept", tags=["部门管理"]) +DeptRouter = APIRouter(route_class=OperationLogRoute, prefix="/dept", tags=["系统管理", "部门管理"]) _DEPT_NS = "dept" - @DeptRouter.get( "/tree", summary="查询部门树", @@ -28,23 +27,9 @@ async def get_dept_tree_controller( search: Annotated[DeptQueryParam, Depends()], auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:dept:query"]))], ) -> JSONResponse: - """ - 查询部门树 - - 参数: - - search (DeptQueryParam): 查询参数模型 - - auth (AuthSchema): 认证信息模型 - - 返回: - - JSONResponse: 包含部门树的响应模型 - - 异常: - - CustomException: 查询部门树失败时抛出异常。 - """ order_by = [{"order": "asc"}] - result_dict_list = await DeptService.tree_service(search=search, auth=auth, order_by=order_by) - return SuccessResponse(data=result_dict_list, msg="查询部门树成功") - + result_dict_tree = await DeptService(auth).tree(search=search, order_by=order_by) + return SuccessResponse(data=result_dict_tree, msg="查询部门树成功") @DeptRouter.get( "/detail/{id}", @@ -55,23 +40,9 @@ async def get_obj_detail_controller( id: Annotated[int, Path(description="部门ID")], auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:dept:detail"]))], ) -> JSONResponse: - """ - 查询部门详情 - - 参数: - - id (int): 部门ID - - auth (AuthSchema): 认证信息模型 - - 返回: - - JSONResponse: 包含部门详情的响应模型 - - 异常: - - CustomException: 查询部门详情失败时抛出异常。 - """ - result_dict = await DeptService.detail_service(id=id, auth=auth) + result_dict = await DeptService(auth).detail(id=id) return SuccessResponse(data=result_dict, msg="查询部门详情成功") - @DeptRouter.post( "/create", summary="创建部门", @@ -81,24 +52,10 @@ async def create_obj_controller( data: DeptCreateSchema, auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:dept:create"]))], ) -> JSONResponse: - """ - 创建部门 - - 参数: - - data (DeptCreateSchema): 创建部门负载模型 - - auth (AuthSchema): 认证信息模型 - - 返回: - - JSONResponse: 包含创建部门结果的响应模型 - - 异常: - - CustomException: 创建部门失败时抛出异常。 - """ - result_dict = await DeptService.create_service(data=data, auth=auth) + result_dict = await DeptService(auth).create(data=data) await FastAPICache.clear(namespace=_DEPT_NS) return SuccessResponse(data=result_dict, msg="创建部门成功") - @DeptRouter.put( "/update/{id}", summary="修改部门", @@ -109,25 +66,10 @@ async def update_obj_controller( id: Annotated[int, Path(description="部门ID")], auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:dept:update"]))], ) -> JSONResponse: - """ - 修改部门 - - 参数: - - data (DeptUpdateSchema): 修改部门负载模型 - - id (int): 部门ID - - auth (AuthSchema): 认证信息模型 - - 返回: - - JSONResponse: 包含修改部门结果的响应模型 - - 异常: - - CustomException: 修改部门失败时抛出异常。 - """ - result_dict = await DeptService.update_service(auth=auth, id=id, data=data) + result_dict = await DeptService(auth).update(id=id, data=data) await FastAPICache.clear(namespace=_DEPT_NS) return SuccessResponse(data=result_dict, msg="修改部门成功") - @DeptRouter.delete( "/delete", summary="删除部门", @@ -137,24 +79,10 @@ async def delete_obj_controller( ids: Annotated[list[int], Body(description="ID列表")], auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:dept:delete"]))], ) -> JSONResponse: - """ - 删除部门 - - 参数: - - ids (list[int]): 部门ID列表 - - auth (AuthSchema): 认证信息模型 - - 返回: - - JSONResponse: 包含删除部门结果的响应模型 - - 异常: - - CustomException: 删除部门失败时抛出异常。 - """ - await DeptService.delete_service(ids=ids, auth=auth) + await DeptService(auth).delete(ids=ids) await FastAPICache.clear(namespace=_DEPT_NS) return SuccessResponse(msg="删除部门成功") - @DeptRouter.patch( "/status/batch", summary="批量修改部门状态", @@ -164,19 +92,6 @@ async def batch_set_available_obj_controller( data: BatchSetAvailable, auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:dept:patch"]))], ) -> JSONResponse: - """ - 批量修改部门状态 - - 参数: - - data (BatchSetAvailable): 批量修改部门状态负载模型 - - auth (AuthSchema): 认证信息模型 - - 返回: - - JSONResponse: 包含批量修改部门状态结果的响应模型 - - 异常: - - CustomException: 批量修改部门状态失败时抛出异常。 - """ - await DeptService.batch_set_available_service(data=data, auth=auth) + await DeptService(auth).batch_set_available(data=data) await FastAPICache.clear(namespace=_DEPT_NS) return SuccessResponse(msg="批量修改部门状态成功") diff --git a/backend/app/api/v1/module_system/dept/model.py b/backend/app/api/v1/module_system/dept/model.py index 125f34dc..6c13cae1 100644 --- a/backend/app/api/v1/module_system/dept/model.py +++ b/backend/app/api/v1/module_system/dept/model.py @@ -26,7 +26,7 @@ class DeptModel(ModelMixin, TenantMixin, UserMixin): "deleted_by", "tenant_by", ] - __permission_strategy__: PermissionFilterStrategy = PermissionFilterStrategy.DEPT_BASED + __permission_strategy__: PermissionFilterStrategy = PermissionFilterStrategy.DEPT_RELATION name: Mapped[str] = mapped_column(String(64), nullable=False, comment="部门名称") status: Mapped[int] = mapped_column(Integer, default=0, nullable=False, comment="状态(0:启动 1:停用)", index=True) diff --git a/backend/app/api/v1/module_system/dept/service.py b/backend/app/api/v1/module_system/dept/service.py index 2a9a6954..ec858aae 100644 --- a/backend/app/api/v1/module_system/dept/service.py +++ b/backend/app/api/v1/module_system/dept/service.py @@ -24,159 +24,78 @@ class DeptService: 提供部门 CRUD、树形结构查询、级联启/禁用、租户配额检查等业务能力。 """ - @classmethod - async def detail_service(cls, auth: AuthSchema, id: int) -> DeptOutSchema: - """ - 获取部门详情 + def __init__(self, auth: AuthSchema) -> None: + self.auth = auth - 参数: - - auth (AuthSchema): 认证信息模型 - - id (int): 部门 ID - - 返回: - - DeptOutSchema: 部门详情响应模型 - """ - dept = await DeptCRUD(auth).get_or_404(id=id) + async def detail(self, id: int) -> DeptOutSchema: + dept = await DeptCRUD(self.auth).get_or_404(id=id) dept_out = DeptOutSchema.model_validate(dept) if dept.parent_id: - parent = await DeptCRUD(auth).get(id=dept.parent_id) + parent = await DeptCRUD(self.auth).get(id=dept.parent_id) if parent: dept_out.parent_name = parent.name return dept_out - @classmethod - async def tree_service( - cls, - auth: AuthSchema, + async def tree( + self, search: DeptQueryParam | None = None, order_by: list[dict] | None = None, ) -> list[dict]: - """ - 获取部门树形列表。 - - 参数: - - auth (AuthSchema): 认证对象。 - - search (DeptQueryParam | None): 查询参数对象。 - - order_by (list[dict] | None): 排序参数。 - - 返回: - - list[dict]: 部门树形列表对象。 - """ - # 使用树形结构查询,预加载children关系 - dept_list = await DeptCRUD(auth).tree_list(search=vars(search) if search else None, order_by=order_by) - # 转换为字典列表(使用树形 Schema),tree_list 已通过 selectin 预加载 children + dept_list = await DeptCRUD(self.auth).tree_list(search=vars(search) if search else None, order_by=order_by) dept_dict_list = [DeptTreeOutSchema.model_validate(dept).model_dump() for dept in dept_list] - # 仅保留根节点,子树已在 model_dump 中递归序列化 return [d for d in dept_dict_list if d.get("parent_id") is None] - @classmethod - async def create_service(cls, auth: AuthSchema, data: DeptCreateSchema) -> DeptOutSchema: - """ - 创建部门。 - - 参数: - - auth (AuthSchema): 认证对象。 - - data (DeptCreateSchema): 部门创建对象。 - - 返回: - - DeptOutSchema: 新创建的部门对象。 - - 异常: - - CustomException: 当部门已存在时抛出。 - """ - dept = await DeptCRUD(auth).get(name=data.name) + async def create(self, data: DeptCreateSchema) -> DeptOutSchema: + dept = await DeptCRUD(self.auth).get(name=data.name) if dept: raise CustomException(msg="创建失败,该数据已存在") - obj = await DeptCRUD(auth).get(code=data.code) + obj = await DeptCRUD(self.auth).get(code=data.code) if obj: raise CustomException(msg="创建失败,编码已存在") # 检查租户配额 from app.api.v1.module_platform.tenant.service import TenantService - await TenantService.check_quota_service(auth, auth.tenant_id, "dept") + await TenantService(self.auth).check_quota(self.auth.tenant_id, "dept") - dept = await DeptCRUD(auth).create(data=data) + dept = await DeptCRUD(self.auth).create(data=data) return DeptOutSchema.model_validate(dept) - @classmethod - async def update_service(cls, auth: AuthSchema, id: int, data: DeptUpdateSchema) -> DeptOutSchema: - """ - 更新部门。 - - 参数: - - auth (AuthSchema): 认证对象。 - - id (int): 部门 ID。 - - data (DeptUpdateSchema): 部门更新对象。 - - 返回: - - DeptOutSchema: 更新后的部门对象。 - - 异常: - - CustomException: 当部门不存在或名称重复时抛出。 - """ - dept = await DeptCRUD(auth).get_or_404(id=id, msg="更新失败,该数据不存在") - exist_dept = await DeptCRUD(auth).get(name=data.name) + async def update(self, id: int, data: DeptUpdateSchema) -> DeptOutSchema: + dept = await DeptCRUD(self.auth).get_or_404(id=id, msg="更新失败,该数据不存在") + exist_dept = await DeptCRUD(self.auth).get(name=data.name) if exist_dept and exist_dept.id != id: raise CustomException(msg="更新失败,名称已存在") - exist_code = await DeptCRUD(auth).get(code=data.code) + exist_code = await DeptCRUD(self.auth).get(code=data.code) if exist_code and exist_code.id != id: raise CustomException(msg="更新失败,编码已存在") - dept = await DeptCRUD(auth).update(id=id, data=data) + dept = await DeptCRUD(self.auth).update(id=id, data=data) dept_out = DeptOutSchema.model_validate(dept) if dept_out.parent_id: - parent = await DeptCRUD(auth).get(id=dept_out.parent_id) + parent = await DeptCRUD(self.auth).get(id=dept_out.parent_id) if parent: dept_out.parent_name = parent.name return dept_out - @classmethod - async def delete_service(cls, auth: AuthSchema, ids: list[int]) -> None: - """ - 删除部门。 - - 参数: - - auth (AuthSchema): 认证对象。 - - ids (list[int]): 部门 ID 列表。 - - 返回: - - None - - 异常: - - CustomException: 当删除对象为空时抛出。 - - CustomException: 当存在子部门时抛出。 - """ + async def delete(self, ids: list[int]) -> None: if len(ids) < 1: raise CustomException(msg="删除失败,删除对象不能为空") # 获取所有部门列表,用于构建树形关系 - all_depts = await DeptCRUD(auth).list() + all_depts = await DeptCRUD(self.auth).list() # 构建子部门ID映射 child_id_map = get_child_id_map(model_list=all_depts) - for id in ids: - # 检查是否有子部门 - if id in child_id_map and child_id_map[id]: + for pid in ids: + if pid in child_id_map and child_id_map[pid]: raise CustomException(msg="存在子部门,不允许删除父部门") - # 执行批量删除操作 - await DeptCRUD(auth).delete(ids=ids) + await DeptCRUD(self.auth).delete(ids=ids) - @classmethod - async def batch_set_available_service(cls, auth: AuthSchema, data: BatchSetAvailable) -> None: - """ - 批量设置部门可用状态。 - - 参数: - - auth (AuthSchema): 认证对象。 - - data (BatchSetAvailable): 批量设置可用状态对象。 - - 返回: - - None - """ - dept_list = await DeptCRUD(auth).list() + async def batch_set_available(self, data: BatchSetAvailable) -> None: + dept_list = await DeptCRUD(self.auth).list() total_ids = [] if data.status == 0: @@ -190,4 +109,4 @@ class DeptService: disable_ids = get_child_recursion(id=dept_id, id_map=id_map) total_ids.extend(disable_ids) - await DeptCRUD(auth).set(ids=total_ids, status=data.status) + await DeptCRUD(self.auth).set(ids=total_ids, status=data.status) diff --git a/backend/app/api/v1/module_system/dict/controller.py b/backend/app/api/v1/module_system/dict/controller.py index 8c7dfbc9..4106fa30 100644 --- a/backend/app/api/v1/module_system/dict/controller.py +++ b/backend/app/api/v1/module_system/dict/controller.py @@ -25,11 +25,10 @@ from .schema import ( ) from .service import DictDataService, DictTypeService -DictRouter = APIRouter(route_class=OperationLogRoute, prefix="/dict", tags=["字典管理"]) +DictRouter = APIRouter(route_class=OperationLogRoute, prefix="/dict", tags=["系统管理", "字典管理"]) _DICT_TYPE_NS = "dict_type" - @DictRouter.get( "/type/detail/{id}", summary="获取字典类型详情", @@ -39,23 +38,9 @@ async def get_type_detail_controller( id: Annotated[int, Path(description="字典类型ID", ge=1)], auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:dict_type:detail"]))], ) -> JSONResponse: - """ - 获取字典类型详情 - - 参数: - - id (int): 字典类型ID - - auth (AuthSchema): 认证信息模型 - - 返回: - - JSONResponse: 包含字典类型详情的响应模型 - - 异常: - - CustomException: 获取字典类型详情失败时抛出异常。 - """ - result_dict = await DictTypeService.detail_service(id=id, auth=auth) + result_dict = await DictTypeService(auth).detail(id=id) return SuccessResponse(data=result_dict, msg="获取字典类型详情成功") - @DictRouter.get( "/type/list", summary="查询字典类型", @@ -66,22 +51,7 @@ async def get_type_list_controller( search: Annotated[DictTypeQueryParam, Depends()], auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:dict_type:query"]))], ) -> JSONResponse: - """ - 查询字典类型 - - 参数: - - page (PaginationQueryParam): 分页参数模型 - - search (DictTypeQueryParam): 查询参数模型 - - auth (AuthSchema): 认证信息模型 - - 返回: - - JSONResponse: 包含查询字典类型结果的响应模型 - - 异常: - - CustomException: 查询字典类型失败时抛出异常。 - """ - result_dict = await DictTypeService.page_service( - auth=auth, + result_dict = await DictTypeService(auth).page( page_no=page.page_no, page_size=page.page_size, search=search, @@ -89,7 +59,6 @@ async def get_type_list_controller( ) return SuccessResponse(data=result_dict, msg="查询字典类型列表成功") - @DictRouter.get( "/type/optionselect", summary="获取全部字典类型", @@ -99,22 +68,9 @@ async def get_type_list_controller( async def get_type_optionselect_controller( auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:dict_type:query"]))], ) -> JSONResponse: - """ - 获取全部字典类型 - - 参数: - - auth (AuthSchema): 认证信息模型 - - 返回: - - JSONResponse: 包含全部字典类型的响应模型 - - 异常: - - CustomException: 获取字典类型列表失败时抛出异常。 - """ - result_dict_list = await DictTypeService.list_service(auth=auth) + result_dict_list = await DictTypeService(auth).list() return SuccessResponse(data=result_dict_list, msg="获取字典类型列表成功") - @DictRouter.post( "/type/create", summary="创建字典类型", @@ -125,25 +81,10 @@ async def create_type_controller( redis: Annotated[Redis, Depends(redis_getter)], auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:dict_type:create"]))], ) -> JSONResponse: - """ - 创建字典类型 - - 参数: - - data (DictTypeCreateSchema): 创建字典类型的入参模型 - - redis (Redis): Redis数据库连接 - - auth (AuthSchema): 认证信息模型 - - 返回: - - JSONResponse: 包含创建字典类型结果的响应模型 - - 异常: - - CustomException: 创建字典类型失败时抛出异常。 - """ - result_dict = await DictTypeService.create_service(auth=auth, redis=redis, data=data) + result_dict = await DictTypeService(auth).create(redis=redis, data=data) await FastAPICache.clear(namespace=_DICT_TYPE_NS) return SuccessResponse(data=result_dict, msg="创建字典类型成功") - @DictRouter.put( "/type/update/{id}", summary="修改字典类型", @@ -155,26 +96,10 @@ async def update_type_controller( id: Annotated[int, Path(description="字典类型ID", ge=1)], auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:dict_type:update"]))], ) -> JSONResponse: - """ - 修改字典类型 - - 参数: - - data (DictTypeUpdateSchema): 修改字典类型的入参模型 - - redis (Redis): Redis数据库连接 - - id (int): 字典类型ID - - auth (AuthSchema): 认证信息模型 - - 返回: - - JSONResponse: 包含修改字典类型结果的响应模型 - - 异常: - - CustomException: 修改字典类型失败时抛出异常。 - """ - result_dict = await DictTypeService.update_service(auth=auth, redis=redis, id=id, data=data) + result_dict = await DictTypeService(auth).update(redis=redis, id=id, data=data) await FastAPICache.clear(namespace=_DICT_TYPE_NS) return SuccessResponse(data=result_dict, msg="修改字典类型成功") - @DictRouter.delete( "/type/delete", summary="删除字典类型", @@ -185,25 +110,10 @@ async def delete_type_controller( ids: Annotated[list[int], Body(description="ID列表")], auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:dict_type:delete"]))], ) -> JSONResponse: - """ - 删除字典类型 - - 参数: - - redis (Redis): Redis数据库连接 - - ids (list[int]): 字典类型ID列表 - - auth (AuthSchema): 认证信息模型 - - 返回: - - JSONResponse: 包含删除字典类型结果的响应模型 - - 异常: - - CustomException: 删除字典类型失败时抛出异常。 - """ - await DictTypeService.delete_service(auth=auth, redis=redis, ids=ids) + await DictTypeService(auth).delete(redis=redis, ids=ids) await FastAPICache.clear(namespace=_DICT_TYPE_NS) return SuccessResponse(msg="删除字典类型成功") - @DictRouter.patch( "/type/status/batch", summary="批量修改字典类型状态", @@ -213,24 +123,10 @@ async def batch_set_available_dict_type_controller( data: BatchSetAvailable, auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:dict_type:patch"]))], ) -> JSONResponse: - """ - 批量修改字典类型状态 - - 参数: - - data (BatchSetAvailable): 批量修改字典类型状态负载模型 - - auth (AuthSchema): 认证信息模型 - - 返回: - - JSONResponse: 包含批量修改字典类型状态结果的响应模型 - - 异常: - - CustomException: 批量修改字典类型状态失败时抛出异常。 - """ - await DictTypeService.set_available_service(auth=auth, data=data) + await DictTypeService(auth).set_available(data=data) await FastAPICache.clear(namespace=_DICT_TYPE_NS) return SuccessResponse(msg="批量修改字典类型状态成功") - @DictRouter.post( "/type/export", summary="导出字典类型", @@ -240,23 +136,10 @@ async def export_type_list_controller( search: Annotated[DictTypeQueryParam, Depends()], auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:dict_type:export"]))], ) -> StreamingResponse: - """ - 导出字典类型 - - 参数: - - search (DictTypeQueryParam): 查询参数模型 - - auth (AuthSchema): 认证信息模型 - - 返回: - - StreamingResponse: 包含导出字典类型结果的响应模型 - - 异常: - - CustomException: 导出字典类型失败时抛出异常。 - """ # 获取全量数据并转为dict列表 - result_dict_list = await DictTypeService.list_service(search=search, auth=auth) + result_dict_list = await DictTypeService(auth).list(search=search) export_data = [item.model_dump() for item in result_dict_list] - export_result = await DictTypeService.export_service(data_list=export_data) + export_result = DictTypeService.export(data_list=export_data) return StreamResponse( data=bytes2file_response(export_result), @@ -264,7 +147,6 @@ async def export_type_list_controller( headers={"Content-Disposition": "attachment; filename=dict_type.xlsx"}, ) - @DictRouter.get( "/data/detail/{id}", summary="获取字典数据详情", @@ -274,23 +156,9 @@ async def get_data_detail_controller( id: Annotated[int, Path(description="字典数据ID", ge=1)], auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:dict_data:detail"]))], ) -> JSONResponse: - """ - 获取字典数据详情 - - 参数: - - id (int): 字典数据ID - - auth (AuthSchema): 认证信息模型 - - 返回: - - JSONResponse: 包含字典数据详情的响应模型 - - 异常: - - CustomException: 获取字典数据详情失败时抛出异常。 - """ - result_dict = await DictDataService.detail_service(id=id, auth=auth) + result_dict = await DictDataService(auth).detail(id=id) return SuccessResponse(data=result_dict, msg="获取字典数据详情成功") - @DictRouter.get( "/data/list", summary="查询字典数据", @@ -301,25 +169,10 @@ async def get_data_list_controller( search: Annotated[DictDataQueryParam, Depends()], auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:dict_data:query"]))], ) -> JSONResponse: - """ - 查询字典数据 - - 参数: - - page (PaginationQueryParam): 分页查询参数模型 - - search (DictDataQueryParam): 查询参数模型 - - auth (AuthSchema): 认证信息模型 - - 返回: - - JSONResponse: 包含字典数据列表的响应模型 - - 异常: - - CustomException: 查询字典数据列表失败时抛出异常。 - """ order_by = [{"order": "asc"}] if page.order_by: order_by = page.order_by - result_dict = await DictDataService.page_service( - auth=auth, + result_dict = await DictDataService(auth).page( page_no=page.page_no, page_size=page.page_size, search=search, @@ -327,7 +180,6 @@ async def get_data_list_controller( ) return SuccessResponse(data=result_dict, msg="查询字典数据列表成功") - @DictRouter.post( "/data/create", summary="创建字典数据", @@ -338,24 +190,9 @@ async def create_data_controller( redis: Annotated[Redis, Depends(redis_getter)], auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:dict_data:create"]))], ) -> JSONResponse: - """ - 创建字典数据 - - 参数: - - data (DictDataCreateSchema): 创建字典数据负载模型 - - redis (Redis): Redis数据库连接 - - auth (AuthSchema): 认证信息模型 - - 返回: - - JSONResponse: 包含创建字典数据结果的响应模型 - - 异常: - - CustomException: 创建字典数据失败时抛出异常。 - """ - result_dict = await DictDataService.create_service(auth=auth, redis=redis, data=data) + result_dict = await DictDataService(auth).create(redis=redis, data=data) return SuccessResponse(data=result_dict, msg="创建字典数据成功") - @DictRouter.put( "/data/update/{id}", summary="修改字典数据", @@ -367,25 +204,9 @@ async def update_data_controller( id: Annotated[int, Path(description="字典数据ID")], auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:dict_data:update"]))], ) -> JSONResponse: - """ - 修改字典数据 - - 参数: - - data (DictDataUpdateSchema): 修改字典数据负载模型 - - redis (Redis): Redis数据库连接 - - id (int): 字典数据ID - - auth (AuthSchema): 认证信息模型 - - 返回: - - JSONResponse: 包含修改字典数据结果的响应模型 - - 异常: - - CustomException: 修改字典数据失败时抛出异常。 - """ - result_dict = await DictDataService.update_service(auth=auth, redis=redis, id=id, data=data) + result_dict = await DictDataService(auth).update(redis=redis, id=id, data=data) return SuccessResponse(data=result_dict, msg="修改字典数据成功") - @DictRouter.delete( "/data/delete", summary="删除字典数据", @@ -396,24 +217,9 @@ async def delete_data_controller( ids: Annotated[list[int], Body(description="ID列表")], auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:dict_data:delete"]))], ) -> JSONResponse: - """ - 删除字典数据 - - 参数: - - redis (Redis): Redis数据库连接 - - ids (list[int]): 字典数据ID列表 - - auth (AuthSchema): 认证信息模型 - - 返回: - - JSONResponse: 包含删除字典数据结果的响应模型 - - 异常: - - CustomException: 删除字典数据失败时抛出异常。 - """ - await DictDataService.delete_service(auth=auth, redis=redis, ids=ids) + await DictDataService(auth).delete(redis=redis, ids=ids) return SuccessResponse(msg="删除字典数据成功") - @DictRouter.patch( "/data/status/batch", summary="批量修改字典数据状态", @@ -423,23 +229,9 @@ async def batch_set_available_dict_data_controller( data: BatchSetAvailable, auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:dict_data:patch"]))], ) -> JSONResponse: - """ - 批量修改字典数据状态 - - 参数: - - data (BatchSetAvailable): 批量修改字典数据状态负载模型 - - auth (AuthSchema): 认证信息模型 - - 返回: - - JSONResponse: 包含批量修改字典数据状态结果的响应模型 - - 异常: - - CustomException: 批量修改字典数据状态失败时抛出异常。 - """ - await DictDataService.set_available_service(auth=auth, data=data) + await DictDataService(auth).set_available(data=data) return SuccessResponse(msg="批量修改字典数据状态成功") - @DictRouter.post( "/data/export", summary="导出字典数据", @@ -450,23 +242,9 @@ async def export_data_list_controller( page: Annotated[PaginationQueryParam, Depends()], auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:dict_data:export"]))], ) -> StreamingResponse: - """ - 导出字典数据 - - 参数: - - search (DictDataQueryParam): 查询参数模型 - - page (PaginationQueryParam): 分页参数模型 - - auth (AuthSchema): 认证信息模型 - - 返回: - - StreamingResponse: 包含导出字典数据结果的响应模型 - - 异常: - - CustomException: 导出字典数据失败时抛出异常。 - """ - result_dict_list = await DictDataService.list_service(auth=auth, search=search, order_by=page.order_by) + result_dict_list = await DictDataService(auth).list(search=search, order_by=page.order_by) export_data = [item.model_dump() for item in result_dict_list] - export_result = await DictDataService.export_service(data_list=export_data) + export_result = DictDataService.export(data_list=export_data) return StreamResponse( data=bytes2file_response(export_result), @@ -474,26 +252,12 @@ async def export_data_list_controller( headers={"Content-Disposition": "attachment; filename=dice_data.xlsx"}, ) - @DictRouter.get( "/data/info/{dict_type}", summary="根据字典类型获取数据", response_model=ResponseSchema[list[DictDataOutSchema]], ) async def get_init_dict_data_controller(dict_type: str, redis: Annotated[Redis, Depends(redis_getter)]) -> JSONResponse: - """ - 根据字典类型获取数据 - - 参数: - - dict_type (str): 字典类型 - - redis (Redis): Redis数据库连接 - - 返回: - - JSONResponse: 包含根据字典类型获取数据结果的响应模型 - - 异常: - - CustomException: 根据字典类型获取数据失败时抛出异常。 - """ - dict_data_query_result = await DictDataService.get_init_cache_service(redis=redis, dict_type=dict_type, tenant_id=1) + dict_data_query_result = await DictDataService.get_init_cache(redis=redis, dict_type=dict_type, tenant_id=1) return SuccessResponse(data=dict_data_query_result, msg="获取初始化字典数据成功") diff --git a/backend/app/api/v1/module_system/dict/service.py b/backend/app/api/v1/module_system/dict/service.py index 4ad897dc..03cba9c9 100644 --- a/backend/app/api/v1/module_system/dict/service.py +++ b/backend/app/api/v1/module_system/dict/service.py @@ -27,27 +27,28 @@ class DictTypeService: """ 字典类型管理服务 - 提供字典类型 CRUD、Redis 缓存同步、字典数据联动更新、批量启/禁用、Excel 导出等业务能力。 + 设计:实例方法承载「当前用户上下文 (auth)」,``redis`` 仍是方法参数 + (因为不是每个端点都用到)。调用方写法由 ``XxxService.method_service(auth=...)`` + 改为 ``XxxService(auth).method(...)``。 """ - @classmethod - async def detail_service(cls, auth: AuthSchema, id: int) -> DictTypeOutSchema: + def __init__(self, auth: AuthSchema) -> None: + self.auth = auth + + async def detail(self, id: int) -> DictTypeOutSchema: """ 获取数据字典类型详情 参数: - - auth (AuthSchema): 认证信息模型 - id (int): 数据字典类型ID 返回: - DictTypeOutSchema: 字典类型响应模型 """ - return await DictTypeCRUD(auth).get_or_404(id=id, out_schema=DictTypeOutSchema) + return await DictTypeCRUD(self.auth).get_or_404(id=id, out_schema=DictTypeOutSchema) - @classmethod - async def list_service( - cls, - auth: AuthSchema, + async def list( + self, search: DictTypeQueryParam | None = None, order_by: list[dict] | None = None, ) -> list[DictTypeOutSchema]: @@ -55,20 +56,17 @@ class DictTypeService: 获取数据字典类型列表 参数: - - auth (AuthSchema): 认证信息模型 - search (DictTypeQueryParam | None): 搜索条件模型 - order_by (list[dict] | None): 排序字段列表 返回: - list[DictTypeOutSchema]: 字典类型响应模型列表 """ - obj_list = await DictTypeCRUD(auth).list(search=vars(search) if search else None, order_by=order_by) + obj_list = await DictTypeCRUD(self.auth).list(search=vars(search) if search else None, order_by=order_by) return [DictTypeOutSchema.model_validate(obj) for obj in obj_list] - @classmethod - async def page_service( - cls, - auth: AuthSchema, + async def page( + self, page_no: int, page_size: int, search: DictTypeQueryParam | None = None, @@ -78,17 +76,16 @@ class DictTypeService: 分页查询字典类型(数据库 OFFSET/LIMIT)。 参数: - - auth (AuthSchema): 认证信息模型 - page_no (int): 页码(从 1 开始) - page_size (int): 每页条数 - search (DictTypeQueryParam | None): 查询条件 - order_by (list[dict] | None): 排序字段列表 返回: - - dict: 分页结果(结构由 `CRUD.page` 返回约定) + - dict: 分页结果(结构由 ``CRUD.page`` 返回约定) """ offset = (page_no - 1) * page_size - return await DictTypeCRUD(auth).page( + return await DictTypeCRUD(self.auth).page( offset=offset, limit=page_size, order_by=order_by or [{"id": "asc"}], @@ -96,27 +93,25 @@ class DictTypeService: out_schema=DictTypeOutSchema, ) - @classmethod - async def create_service(cls, auth: AuthSchema, redis: Redis, data: DictTypeCreateSchema) -> DictTypeOutSchema: + async def create(self, redis: Redis, data: DictTypeCreateSchema) -> DictTypeOutSchema: """ 创建数据字典类型 参数: - - auth (AuthSchema): 认证信息模型 - redis (Redis): Redis客户端 - data (DictTypeCreateSchema): 数据字典类型创建模型 返回: - DictTypeOutSchema: 字典类型响应模型 """ - exist_obj = await DictTypeCRUD(auth).get(dict_name=data.dict_name) + exist_obj = await DictTypeCRUD(self.auth).get(dict_name=data.dict_name) if exist_obj: raise CustomException(msg="创建失败,该数据已存在") - obj = await DictTypeCRUD(auth).create(data=data) + obj = await DictTypeCRUD(self.auth).create(data=data) new_obj_dict = DictTypeOutSchema.model_validate(obj) - redis_key = f"{RedisInitKeyConfig.SYSTEM_DICT.key}:{auth.user.tenant_id}:{data.dict_type}" + redis_key = f"{RedisInitKeyConfig.SYSTEM_DICT.key}:{self.auth.user.tenant_id}:{data.dict_type}" try: await RedisCURD(redis).set( @@ -131,10 +126,8 @@ class DictTypeService: return new_obj_dict - @classmethod - async def update_service( - cls, - auth: AuthSchema, + async def update( + self, redis: Redis, id: int, data: DictTypeUpdateSchema, @@ -143,7 +136,6 @@ class DictTypeService: 更新数据字典类型 参数: - - auth (AuthSchema): 认证信息模型 - redis (Redis): Redis客户端 - id (int): 数据字典类型ID - data (DictTypeUpdateSchema): 数据字典类型更新模型 @@ -151,13 +143,13 @@ class DictTypeService: 返回: - DictTypeOutSchema: 字典类型响应模型 """ - exist_obj = await DictTypeCRUD(auth).get_or_404(id=id, msg="更新失败,该数据不存在") + exist_obj = await DictTypeCRUD(self.auth).get_or_404(id=id, msg="更新失败,该数据不存在") if exist_obj.dict_name != data.dict_name: raise CustomException(msg="更新失败,数据字典类型名称不可以修改") # 如果字典类型修改或状态变更,则修改对应字典数据的类型和状态 if exist_obj.dict_type != data.dict_type or exist_obj.status != data.status: - exist_obj_type_list = await DictDataCRUD(auth).list(search={"dict_type": exist_obj.dict_type}) + exist_obj_type_list = await DictDataCRUD(self.auth).list(search={"dict_type": exist_obj.dict_type}) if exist_obj_type_list: for item in exist_obj_type_list: item.dict_type = data.dict_type @@ -173,16 +165,16 @@ class DictTypeService: status=data.status, description=item.description, ) - await DictDataCRUD(auth).update(id=item.id, data=dict_data) + await DictDataCRUD(self.auth).update(id=item.id, data=dict_data) - obj = await DictTypeCRUD(auth).update(id=id, data=data) + obj = await DictTypeCRUD(self.auth).update(id=id, data=data) new_obj_dict = DictTypeOutSchema.model_validate(obj) - redis_key = f"{RedisInitKeyConfig.SYSTEM_DICT.key}:{auth.user.tenant_id}:{data.dict_type}" + redis_key = f"{RedisInitKeyConfig.SYSTEM_DICT.key}:{self.auth.user.tenant_id}:{data.dict_type}" try: # 获取当前字典类型的所有字典数据,确保包含最新状态 - dict_data_list = await DictDataCRUD(auth).list(search={"dict_type": data.dict_type}) + dict_data_list = await DictDataCRUD(self.auth).list(search={"dict_type": data.dict_type}) dict_data = [DictDataOutSchema.model_validate(row).model_dump(mode="json") for row in dict_data_list if row] value = json.dumps(dict_data, ensure_ascii=False) @@ -198,13 +190,11 @@ class DictTypeService: return new_obj_dict - @classmethod - async def delete_service(cls, auth: AuthSchema, redis: Redis, ids: list[int]) -> None: + async def delete(self, redis: Redis, ids: list[int]) -> None: """ 删除数据字典类型 参数: - - auth (AuthSchema): 认证信息模型 - redis (Redis): Redis客户端 - ids (list[int]): 数据字典类型ID列表 @@ -213,45 +203,43 @@ class DictTypeService: """ if len(ids) < 1: raise CustomException(msg="删除失败,删除对象不能为空") - existing = await DictTypeCRUD(auth).list(search={"id": ("in", ids)}) + existing = await DictTypeCRUD(self.auth).list(search={"id": ("in", ids)}) existing_map = {obj.id: obj for obj in existing} for nid in ids: if nid not in existing_map: raise CustomException(msg="删除失败,该数据不存在") exist_obj = existing_map[nid] # 检查是否有字典数据 - exist_obj_type_list = await DictDataCRUD(auth).list(search={"dict_type": exist_obj.dict_type}) + exist_obj_type_list = await DictDataCRUD(self.auth).list(search={"dict_type": exist_obj.dict_type}) if len(exist_obj_type_list) > 0: # 如果有字典数据,不能删除 raise CustomException(msg="删除失败,该数据字典类型下存在字典数据") # 删除Redis缓存 - redis_key = f"{RedisInitKeyConfig.SYSTEM_DICT.key}:{auth.user.tenant_id}:{exist_obj.dict_type}" + redis_key = f"{RedisInitKeyConfig.SYSTEM_DICT.key}:{self.auth.user.tenant_id}:{exist_obj.dict_type}" try: await RedisCURD(redis).delete(redis_key) logger.info(f"删除字典类型成功: {nid}") except Exception as e: logger.error(f"删除字典类型失败: {e}") raise CustomException(msg="同步删除字典缓存失败") from e - await DictTypeCRUD(auth).delete(ids=ids) + await DictTypeCRUD(self.auth).delete(ids=ids) - @classmethod - async def set_available_service(cls, auth: AuthSchema, data: BatchSetAvailable) -> None: + async def set_available(self, data: BatchSetAvailable) -> None: """ 设置数据字典类型状态 参数: - - auth (AuthSchema): 认证信息模型 - data (BatchSetAvailable): 批量设置状态模型 返回: - None """ - await DictTypeCRUD(auth).set(ids=data.ids, status=data.status) + await DictTypeCRUD(self.auth).set(ids=data.ids, status=data.status) - @classmethod - async def export_service(cls, data_list: list[dict]) -> bytes: + @staticmethod + def export(data_list: list[dict]) -> bytes: """ - 导出数据字典类型列表 + 导出数据字典类型列表(无状态工具方法) 参数: - data_list (list[dict]): 数据字典类型列表 @@ -284,27 +272,26 @@ class DictDataService: """ 字典数据管理服务 - 提供字典数据 CRUD、Redis 缓存同步、初始化字典、批量启/禁用、Excel 导出等业务能力。 + 设计同 DictTypeService:实例方法 + ``__init__(auth)``。 """ - @classmethod - async def detail_service(cls, auth: AuthSchema, id: int) -> DictDataOutSchema: + def __init__(self, auth: AuthSchema) -> None: + self.auth = auth + + async def detail(self, id: int) -> DictDataOutSchema: """ 获取数据字典数据详情 参数: - - auth (AuthSchema): 认证信息模型 - id (int): 数据字典数据ID 返回: - DictDataOutSchema: 字典数据响应模型 """ - return await DictDataCRUD(auth).get_or_404(id=id, out_schema=DictDataOutSchema) + return await DictDataCRUD(self.auth).get_or_404(id=id, out_schema=DictDataOutSchema) - @classmethod - async def list_service( - cls, - auth: AuthSchema, + async def list( + self, search: DictDataQueryParam | None = None, order_by: list[dict] | None = None, ) -> list[DictDataOutSchema]: @@ -312,20 +299,17 @@ class DictDataService: 获取数据字典数据列表 参数: - - auth (AuthSchema): 认证信息模型 - search (DictDataQueryParam | None): 搜索条件模型 - order_by (list[dict] | None): 排序字段列表 返回: - list[DictDataOutSchema]: 字典数据响应模型列表 """ - obj_list = await DictDataCRUD(auth).list(search=vars(search) if search else None, order_by=order_by) + obj_list = await DictDataCRUD(self.auth).list(search=vars(search) if search else None, order_by=order_by) return [DictDataOutSchema.model_validate(obj) for obj in obj_list] - @classmethod - async def page_service( - cls, - auth: AuthSchema, + async def page( + self, page_no: int, page_size: int, search: DictDataQueryParam | None = None, @@ -335,17 +319,16 @@ class DictDataService: 分页查询字典数据(数据库 OFFSET/LIMIT)。 参数: - - auth (AuthSchema): 认证信息模型 - page_no (int): 页码(从 1 开始) - page_size (int): 每页条数 - search (DictDataQueryParam | None): 查询条件 - order_by (list[dict] | None): 排序字段列表 返回: - - dict: 分页结果(结构由 `CRUD.page` 返回约定) + - dict: 分页结果(结构由 ``CRUD.page`` 返回约定) """ offset = (page_no - 1) * page_size - return await DictDataCRUD(auth).page( + return await DictDataCRUD(self.auth).page( offset=offset, limit=page_size, order_by=order_by or [{"id": "asc"}], @@ -353,10 +336,10 @@ class DictDataService: out_schema=DictDataOutSchema, ) - @classmethod - async def init_cache_service(cls, redis: Redis) -> None: + @staticmethod + async def init_cache(redis: Redis) -> None: """ - 应用初始化: 获取所有字典类型对应的字典数据信息并按租户缓存。 + 应用初始化: 获取所有字典类型对应的字典数据信息并按租户缓存(无 auth)。 参数: - redis (Redis): Redis客户端 @@ -367,8 +350,8 @@ class DictDataService: try: async with async_db_session() as session: async with session.begin(): - auth = AuthSchema(db=session, check_data_scope=False) - obj_list = await DictTypeCRUD(auth).list() + init_auth = AuthSchema(db=session, check_data_scope=False) + obj_list = await DictTypeCRUD(init_auth).list() if not obj_list: logger.warning("未找到任何字典类型数据") return @@ -377,8 +360,14 @@ class DictDataService: dict_type = obj.dict_type tenant_id = obj.tenant_id try: - dict_data_list = await DictDataCRUD(auth).list(search={"dict_type": dict_type, "tenant_id": tenant_id}) - dict_data = [DictDataOutSchema.model_validate(row).model_dump(mode="json") for row in dict_data_list if row] + dict_data_list = await DictDataCRUD(init_auth).list( + search={"dict_type": dict_type, "tenant_id": tenant_id} + ) + dict_data = [ + DictDataOutSchema.model_validate(row).model_dump(mode="json") + for row in dict_data_list + if row + ] redis_key = f"{RedisInitKeyConfig.SYSTEM_DICT.key}:{tenant_id}:{dict_type}" value = json.dumps(dict_data, ensure_ascii=False) await RedisCURD(redis).set( @@ -393,10 +382,10 @@ class DictDataService: logger.error(f"字典初始化过程发生错误: {e}") raise CustomException(msg="字典数据初始化失败") from e - @classmethod - async def get_init_cache_service(cls, redis: Redis, dict_type: str, tenant_id: int = 1) -> list[dict]: + @staticmethod + async def get_init_cache(redis: Redis, dict_type: str, tenant_id: int = 1) -> list[dict]: """ - 从缓存获取字典数据列表信息 + 从缓存获取字典数据列表信息(无 auth)。 参数: - redis (Redis): Redis客户端 @@ -419,7 +408,7 @@ class DictDataService: elif isinstance(obj_list_dict, list): return obj_list_dict - await cls.init_cache_service(redis) + await DictDataService.init_cache(redis) redis_key = f"{RedisInitKeyConfig.SYSTEM_DICT.key}:{tenant_id}:{dict_type}" obj_list_dict = await RedisCURD(redis).get(redis_key) if not obj_list_dict: @@ -437,13 +426,11 @@ class DictDataService: logger.error(f"获取字典缓存失败: {e!s}") raise CustomException(msg="获取字典数据失败") from e - @classmethod - async def create_service(cls, auth: AuthSchema, redis: Redis, data: DictDataCreateSchema) -> DictDataOutSchema: + async def create(self, redis: Redis, data: DictDataCreateSchema) -> DictDataOutSchema: """ 创建数据字典数据 参数: - - auth (AuthSchema): 认证信息模型 - redis (Redis): Redis客户端 - data (DictDataCreateSchema): 数据字典数据创建模型 @@ -451,21 +438,21 @@ class DictDataService: - DictDataOutSchema: 字典数据响应模型 """ # 检查相同字典类型下dict_label是否已存在 - exist_label_obj = await DictDataCRUD(auth).get(dict_type=data.dict_type, dict_label=data.dict_label) + exist_label_obj = await DictDataCRUD(self.auth).get(dict_type=data.dict_type, dict_label=data.dict_label) if exist_label_obj: raise CustomException(msg=f'创建失败,该字典类型下的字典标签"{data.dict_label}"已存在') # 检查相同字典类型下dict_value是否已存在 - exist_value_obj = await DictDataCRUD(auth).get(dict_type=data.dict_type, dict_value=data.dict_value) + exist_value_obj = await DictDataCRUD(self.auth).get(dict_type=data.dict_type, dict_value=data.dict_value) if exist_value_obj: raise CustomException(msg=f'创建失败,该字典类型下的字典键值"{data.dict_value}"已存在') - obj = await DictDataCRUD(auth).create(data=data) + obj = await DictDataCRUD(self.auth).create(data=data) - redis_key = f"{RedisInitKeyConfig.SYSTEM_DICT.key}:{auth.user.tenant_id}:{data.dict_type}" + redis_key = f"{RedisInitKeyConfig.SYSTEM_DICT.key}:{self.auth.user.tenant_id}:{data.dict_type}" try: # 获取当前字典类型的所有字典数据 - dict_data_list = await DictDataCRUD(auth).list(search={"dict_type": data.dict_type}) + dict_data_list = await DictDataCRUD(self.auth).list(search={"dict_type": data.dict_type}) dict_data = [DictDataOutSchema.model_validate(row).model_dump(mode="json") for row in dict_data_list if row] value = json.dumps(dict_data, ensure_ascii=False) @@ -481,10 +468,8 @@ class DictDataService: return DictDataOutSchema.model_validate(obj) - @classmethod - async def update_service( - cls, - auth: AuthSchema, + async def update( + self, redis: Redis, id: int, data: DictDataUpdateSchema, @@ -493,7 +478,6 @@ class DictDataService: 更新数据字典数据 参数: - - auth (AuthSchema): 认证信息模型 - redis (Redis): Redis客户端 - id (int): 数据字典数据ID - data (DictDataUpdateSchema): 数据字典数据更新模型 @@ -501,27 +485,27 @@ class DictDataService: 返回: - DictDataOutSchema: 字典数据响应模型 """ - exist_obj = await DictDataCRUD(auth).get_or_404(id=id, msg="更新失败,该数据不存在") + exist_obj = await DictDataCRUD(self.auth).get_or_404(id=id, msg="更新失败,该数据不存在") # 检查相同字典类型下dict_label是否已存在(排除当前记录) if exist_obj.dict_label != data.dict_label: - exist_label_obj = await DictDataCRUD(auth).get(dict_type=data.dict_type, dict_label=data.dict_label) + exist_label_obj = await DictDataCRUD(self.auth).get(dict_type=data.dict_type, dict_label=data.dict_label) if exist_label_obj: raise CustomException(msg=f'更新失败,该字典类型下的字典标签"{data.dict_label}"已存在') # 检查相同字典类型下dict_value是否已存在(排除当前记录) if exist_obj.dict_value != data.dict_value: - exist_value_obj = await DictDataCRUD(auth).get(dict_type=data.dict_type, dict_value=data.dict_value) + exist_value_obj = await DictDataCRUD(self.auth).get(dict_type=data.dict_type, dict_value=data.dict_value) if exist_value_obj: raise CustomException(msg=f'更新失败,该字典类型下的字典键值"{data.dict_value}"已存在') # 如果字典类型变更,仅刷新旧类型缓存,不联动字典类型状态 if exist_obj.dict_type != data.dict_type: - dict_type = await DictTypeCRUD(auth).get(dict_type=exist_obj.dict_type) + dict_type = await DictTypeCRUD(self.auth).get(dict_type=exist_obj.dict_type) if dict_type: - redis_key = f"{RedisInitKeyConfig.SYSTEM_DICT.key}:{auth.user.tenant_id}:{dict_type.dict_type}" + redis_key = f"{RedisInitKeyConfig.SYSTEM_DICT.key}:{self.auth.user.tenant_id}:{dict_type.dict_type}" try: - dict_data_list = await DictDataCRUD(auth).list(search={"dict_type": dict_type.dict_type}) + dict_data_list = await DictDataCRUD(self.auth).list(search={"dict_type": dict_type.dict_type}) dict_data = [DictDataOutSchema.model_validate(row).model_dump(mode="json") for row in dict_data_list if row] value = json.dumps(dict_data, ensure_ascii=False) await RedisCURD(redis).set( @@ -530,15 +514,16 @@ class DictDataService: expire=None, ) except Exception as e: - logger.error(f"更新字典数据类型变更时刷新旧缓存失败: {e}") + logger.error(f"刷新旧字典缓存失败: {e}") + raise CustomException(msg="同步旧字典数据缓存失败") from e - obj = await DictDataCRUD(auth).update(id=id, data=data) - redis_key = f"{RedisInitKeyConfig.SYSTEM_DICT.key}:{auth.user.tenant_id}:{data.dict_type}" + obj = await DictDataCRUD(self.auth).update(id=id, data=data) + + # 刷新新字典类型缓存 + redis_key = f"{RedisInitKeyConfig.SYSTEM_DICT.key}:{self.auth.user.tenant_id}:{data.dict_type}" try: - # 获取当前字典类型的所有字典数据 - dict_data_list = await DictDataCRUD(auth).list(search={"dict_type": data.dict_type}) + dict_data_list = await DictDataCRUD(self.auth).list(search={"dict_type": data.dict_type}) dict_data = [DictDataOutSchema.model_validate(row).model_dump(mode="json") for row in dict_data_list if row] - value = json.dumps(dict_data, ensure_ascii=False) await RedisCURD(redis).set( key=redis_key, @@ -552,76 +537,59 @@ class DictDataService: return DictDataOutSchema.model_validate(obj) - @classmethod - async def delete_service(cls, auth: AuthSchema, redis: Redis, ids: list[int]) -> None: + async def delete(self, redis: Redis, ids: list[int]) -> None: """ 删除数据字典数据 参数: - - auth (AuthSchema): 认证信息模型 - redis (Redis): Redis客户端 - ids (list[int]): 数据字典数据ID列表 返回: - None """ - try: - if len(ids) < 1: - raise CustomException(msg="删除失败,删除对象不能为空") + if len(ids) < 1: + raise CustomException(msg="删除失败,删除对象不能为空") + existing = await DictDataCRUD(self.auth).list(search={"id": ("in", ids)}) + existing_map = {obj.id: obj for obj in existing} + for nid in ids: + if nid not in existing_map: + raise CustomException(msg="删除失败,该数据不存在") + exist_obj = existing_map[nid] + # 删除该字典类型缓存中的对应项 + redis_key = f"{RedisInitKeyConfig.SYSTEM_DICT.key}:{self.auth.user.tenant_id}:{exist_obj.dict_type}" + try: + # 重新拉取该类型所有字典数据并写回缓存(保持一致) + dict_data_list = await DictDataCRUD(self.auth).list(search={"dict_type": exist_obj.dict_type}) + dict_data = [DictDataOutSchema.model_validate(row).model_dump(mode="json") for row in dict_data_list if row] + value = json.dumps(dict_data, ensure_ascii=False) + await RedisCURD(redis).set( + key=redis_key, + value=value, + expire=None, + ) + logger.info(f"删除字典数据并刷新缓存: {nid}") + except Exception as e: + logger.error(f"删除字典数据刷新缓存失败: {e}") + raise CustomException(msg="同步删除字典数据缓存失败") from e + await DictDataCRUD(self.auth).delete(ids=ids) - # 批量查询所有待删除项 - existing = await DictDataCRUD(auth).list(search={"id": ("in", ids)}) - existing_map = {obj.id: obj for obj in existing} - for nid in ids: - if nid not in existing_map: - raise CustomException(msg=f"{nid} 删除失败,该字典数据不存在") - exist_obj = existing_map[nid] - # 系统默认字典数据不允许删除 - if exist_obj.is_default: - raise CustomException(msg=f"删除失败,ID为{nid}的系统默认字典数据不允许删除") - - # 从批量查询结果中收集缓存键 - dict_types_to_clear = {obj.dict_type for obj in existing} - - # 执行删除操作 - await DictDataCRUD(auth).delete(ids=ids) - - # 清除缓存 - for dict_type in dict_types_to_clear: - try: - redis_key = f"{RedisInitKeyConfig.SYSTEM_DICT.key}:{auth.user.tenant_id}:{dict_type}" - await RedisCURD(redis).delete(redis_key) - logger.info(f"清除字典缓存成功: {dict_type}") - except Exception as e: - logger.warning(f"清除字典缓存失败: {e}") - # 缓存清除失败不影响删除操作 - - logger.info(f"删除字典数据成功,ID列表: {ids}") - - except CustomException: - raise - except Exception as e: - logger.error(f"删除字典数据失败: {e!s}") - raise CustomException(msg="删除字典数据失败") from e - - @classmethod - async def set_available_service(cls, auth: AuthSchema, data: BatchSetAvailable) -> None: + async def set_available(self, data: BatchSetAvailable) -> None: """ - 批量修改数据字典数据状态 + 设置数据字典数据状态 参数: - - auth (AuthSchema): 认证信息模型 - - data (BatchSetAvailable): 批量修改数据字典数据状态负载模型 + - data (BatchSetAvailable): 批量设置状态模型 返回: - None """ - await DictDataCRUD(auth).set(ids=data.ids, status=data.status) + await DictDataCRUD(self.auth).set(ids=data.ids, status=data.status) - @classmethod - async def export_service(cls, data_list: list[dict]) -> bytes: + @staticmethod + def export(data_list: list[dict]) -> bytes: """ - 导出数据字典数据列表 + 导出数据字典数据列表(无状态工具方法) 参数: - data_list (list[dict]): 数据字典数据列表 @@ -631,13 +599,10 @@ class DictDataService: """ mapping_dict = { "id": "编号", - "dict_sort": "字典排序", + "dict_type": "字典类型", "dict_label": "字典标签", "dict_value": "字典键值", - "dict_type": "字典类型", - "css_class": "样式属性", - "list_class": "表格回显样式", - "is_default": "是否默认", + "dict_sort": "字典排序", "status": "状态", "description": "备注", "created_time": "创建时间", @@ -649,9 +614,10 @@ class DictDataService: # 复制数据并转换状态 data = data_list.copy() for item in data: - # 处理状态 item["status"] = "启用" if item.get("status") == 0 else "停用" - # 处理是否默认 - item["is_default"] = "是" if item.get("is_default") else "否" + if item.get("is_default") is True: + item["is_default"] = "是" + else: + item["is_default"] = "否" return ExcelUtil.export_list2excel(list_data=data, mapping_dict=mapping_dict) diff --git a/backend/app/api/v1/module_system/log/controller.py b/backend/app/api/v1/module_system/log/controller.py index 9d561e86..52fdd56c 100644 --- a/backend/app/api/v1/module_system/log/controller.py +++ b/backend/app/api/v1/module_system/log/controller.py @@ -21,7 +21,7 @@ from .schema import ( ) from .service import LoginLogService, OperationLogService -LogRouter = APIRouter(route_class=OperationLogRoute, prefix="/log", tags=["日志管理"]) +LogRouter = APIRouter(route_class=OperationLogRoute, prefix="/log", tags=["系统管理", "日志管理"]) @LogRouter.get( @@ -33,17 +33,7 @@ async def get_log_detail_controller( id: Annotated[int, Path(description="登录日志ID")], auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:login_log:query"]))], ) -> JSONResponse: - """ - 获取登录日志详情 - - 参数: - - id (int): 登录日志 ID。 - - auth (AuthSchema): 认证信息模型。 - - 返回: - - JSONResponse: 包含登录日志详情的 JSON 响应。 - """ - result_dict = await LoginLogService.detail_service(id=id, auth=auth) + result_dict = await LoginLogService(auth).detail(id=id) return SuccessResponse(data=result_dict, msg="获取登录日志详情成功") @@ -57,19 +47,7 @@ async def get_log_list_controller( search: Annotated[LoginLogQueryParam, Depends()], auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:login_log:query"]))], ) -> JSONResponse: - """ - 查询登录日志列表 - - 参数: - - page (PaginationQueryParam): 分页查询参数。 - - search (LoginLogQueryParam): 查询筛选参数。 - - auth (AuthSchema): 认证信息模型。 - - 返回: - - JSONResponse: 包含分页登录日志列表的 JSON 响应。 - """ - result_dict = await LoginLogService.page_service( - auth=auth, + result_dict = await LoginLogService(auth).page( page_no=page.page_no, page_size=page.page_size, search=search, @@ -87,17 +65,7 @@ async def create_log_controller( data: LoginLogCreateSchema, auth: Annotated[AuthSchema, Depends(get_current_user)], ) -> JSONResponse: - """ - 创建登录日志 - - 参数: - - data (LoginLogCreateSchema): 登录日志创建参数。 - - auth (AuthSchema): 认证信息模型。 - - 返回: - - JSONResponse: 包含创建后的登录日志详情的 JSON 响应。 - """ - result_dict = await LoginLogService.create_service(auth=auth, data=data) + result_dict = await LoginLogService(auth).create(data=data) return SuccessResponse(data=result_dict, msg="创建登录日志成功") @@ -110,17 +78,7 @@ async def delete_log_controller( ids: Annotated[list[int], Body(description="ID列表")], auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:login_log:delete"]))], ) -> JSONResponse: - """ - 删除登录日志 - - 参数: - - ids (list[int]): 日志 ID 列表。 - - auth (AuthSchema): 认证信息模型。 - - 返回: - - JSONResponse: 删除结果。 - """ - await LoginLogService.delete_service(auth=auth, ids=ids) + await LoginLogService(auth).delete(ids=ids) return SuccessResponse(msg="删除登录日志成功") @@ -135,17 +93,7 @@ async def get_operation_log_detail_controller( id: Annotated[int, Path(gt=0)], auth: Annotated[AuthSchema, Depends(get_current_user)], ): - """ - 获取操作日志详情 - - 参数: - - id (int): 操作日志 ID。 - - auth (AuthSchema): 认证信息模型。 - - 返回: - - JSONResponse: 包含操作日志详情的 JSON 响应。 - """ - result_dict = await OperationLogService.detail_service(auth=auth, id=id) + result_dict = await OperationLogService(auth).detail(id=id) return SuccessResponse(data=result_dict, msg="获取操作日志详情成功") @@ -161,23 +109,11 @@ async def list( search: Annotated[OperationLogQueryParam, Depends()], auth: Annotated[AuthSchema, Depends(get_current_user)], ): - """ - 获取操作日志列表 - - 参数: - - page (PaginationQueryParam): 分页查询参数。 - - search (OperationLogQueryParam): 查询筛选参数。 - - auth (AuthSchema): 认证信息模型。 - - 返回: - - JSONResponse: 包含分页操作日志列表的 JSON 响应。 - """ - result_dict = await OperationLogService.page_service( - auth, - page.page_no, - page.page_size, - search.model_dump(exclude_none=True), - page.order_by, + result_dict = await OperationLogService(auth).page( + page_no=page.page_no, + page_size=page.page_size, + search=search, + order_by=page.order_by, ) return SuccessResponse(data=result_dict, msg="查询操作日志列表成功") @@ -192,17 +128,7 @@ async def create_operation_log_controller( data: OperationLogCreateSchema, auth: Annotated[AuthSchema, Depends(get_current_user)], ): - """ - 创建操作日志 - - 参数: - - data (OperationLogCreateSchema): 操作日志创建参数。 - - auth (AuthSchema): 认证信息模型。 - - 返回: - - JSONResponse: 包含创建后的操作日志详情的 JSON 响应。 - """ - result_dict = await OperationLogService.create_service(auth=auth, data=data) + result_dict = await OperationLogService(auth).create(data=data) return SuccessResponse(data=result_dict, msg="创建操作日志成功") @@ -217,15 +143,5 @@ async def delete( data: BatchDelete, auth: Annotated[AuthSchema, Depends(get_current_user)], ): - """ - 删除操作日志 - - 参数: - - data (BatchDelete): 批量删除参数(ID 列表)。 - - auth (AuthSchema): 认证信息模型。 - - 返回: - - JSONResponse: 删除结果。 - """ - await OperationLogService.delete_service(auth=auth, ids=data.ids) + await OperationLogService(auth).delete(ids=data.ids) return SuccessResponse(msg="删除操作日志成功") diff --git a/backend/app/api/v1/module_system/log/service.py b/backend/app/api/v1/module_system/log/service.py index a10ad32f..203e9ff7 100644 --- a/backend/app/api/v1/module_system/log/service.py +++ b/backend/app/api/v1/module_system/log/service.py @@ -16,99 +16,75 @@ from .schema import ( class LoginLogService: - """ - 登录日志管理服务 + """登录日志管理服务""" - 提供登录日志 CRUD、清理过期日志等业务能力。 - """ + def __init__(self, auth: AuthSchema) -> None: + self.auth = auth - @classmethod - async def detail_service(cls, auth: AuthSchema, id: int) -> LoginLogDetailOutSchema: - return await LoginLogCRUD(auth).get_or_404(id=id, out_schema=LoginLogDetailOutSchema) + async def detail(self, id: int) -> LoginLogDetailOutSchema: + return await LoginLogCRUD(self.auth).get_or_404(id=id, out_schema=LoginLogDetailOutSchema) - @classmethod - async def page_service( - cls, - auth: AuthSchema, + async def page( + self, page_no: int, page_size: int, search: LoginLogQueryParam | None = None, order_by: list[dict[str, str]] | None = None, ) -> dict: - search_dict = vars(search) if search else None - order_by_list = order_by or [{"updated_time": "desc"}] - offset = (page_no - 1) * page_size - - result = await LoginLogCRUD(auth).page( - offset=offset, + return await LoginLogCRUD(self.auth).page( + offset=(page_no - 1) * page_size, limit=page_size, - order_by=order_by_list, - search=search_dict, + order_by=order_by or [{"updated_time": "desc"}], + search=vars(search) if search else None, out_schema=LoginLogOutSchema, ) - return result - @classmethod - async def create_service(cls, auth: AuthSchema, data: LoginLogCreateSchema) -> LoginLogDetailOutSchema: - obj = await LoginLogCRUD(auth).create(data=data) + async def create(self, data: LoginLogCreateSchema) -> LoginLogDetailOutSchema: + obj = await LoginLogCRUD(self.auth).create(data=data) if not obj: raise CustomException(msg="创建失败") return LoginLogDetailOutSchema.model_validate(obj) - @classmethod - async def delete_service(cls, auth: AuthSchema, ids: list[int]) -> None: + async def delete(self, ids: list[int]) -> None: if len(ids) < 1: raise CustomException(msg="删除失败,删除对象不能为空") - existing = await LoginLogCRUD(auth).list(search={"id": ("in", ids)}) + existing = await LoginLogCRUD(self.auth).list(search={"id": ("in", ids)}) existing_map = {obj.id for obj in existing} for nid in ids: if nid not in existing_map: raise CustomException(msg=f"删除失败,ID为{nid}的数据不存在") - await LoginLogCRUD(auth).delete(ids=ids) + await LoginLogCRUD(self.auth).delete(ids=ids) class OperationLogService: - """ - 操作日志管理服务 + """操作日志管理服务""" - 提供操作日志记录、分页查询、清理过期日志等业务能力。 - """ + def __init__(self, auth: AuthSchema) -> None: + self.auth = auth @staticmethod async def cleanup_operation_log() -> None: - """ - 定时任务:清理超过保留期的操作日志和登录日志。 - - 清理 created_time < now - retention_days 的记录。 - 保留期从全局参数 `operation_log_retention_days` 读取,默认 90 天。 - - 返回: - - bool: 清理完成返回 True。 - """ from datetime import datetime, timedelta - from sqlalchemy import delete + from sqlalchemy import delete, select from app.core.database import async_db_session from .model import LoginLogModel, OperationLogModel - retention_days = 90 # 默认 90 天,可通过系统参数覆盖 - # 尝试从系统参数读取自定义保留期 + retention_days = 90 try: from app.api.v1.module_system.params.model import ParamsModel async with async_db_session() as _s: - from sqlalchemy import select - result = await _s.execute(select(ParamsModel.config_value).where(ParamsModel.config_key == "operation_log_retention_days").limit(1)) row = result.scalar() if row is not None: retention_days = int(row) except Exception: - pass # 读取失败使用默认值 + pass cutoff = datetime.now() - timedelta(days=retention_days) async with async_db_session() as session: @@ -122,50 +98,21 @@ class OperationLogService: logger.info(f"操作日志清理完成: 操作日志 {op_result.rowcount} 条, 登录日志 {login_result.rowcount} 条") return True - @classmethod - async def create_service(cls, auth: AuthSchema, data: OperationLogCreateSchema) -> OperationLogDetailOutSchema: - """ - 创建操作日志。 - - 参数: - - auth (AuthSchema): 认证信息模型。 - - data (OperationLogCreateSchema): 操作日志创建模型。 - - 返回: - - OperationLogDetailOutSchema: 新创建的操作日志。 - - 异常: - - CustomException: 创建失败时抛出。 - """ - crud = OperationLogCRUD(auth) + async def create(self, data: OperationLogCreateSchema) -> OperationLogDetailOutSchema: + crud = OperationLogCRUD(self.auth) obj = await crud.create(data=data) if not obj: raise CustomException(msg="创建失败") return OperationLogDetailOutSchema.model_validate(obj) - @classmethod - async def page_service( - cls, - auth: AuthSchema, + async def page( + self, page_no: int, page_size: int, search: OperationLogQueryParam | None = None, order_by: list[dict[str, str]] | None = None, ) -> dict: - """ - 分页查询操作日志。 - - 参数: - - auth (AuthSchema): 认证信息模型。 - - page_no (int): 页码。 - - page_size (int): 每页数量。 - - search (OperationLogQueryParam | None): 查询参数。 - - order_by (list[dict[str, str]] | None): 排序参数。 - - 返回: - - dict: 分页数据。 - """ - crud = OperationLogCRUD(auth) + crud = OperationLogCRUD(self.auth) return await crud.page( offset=(page_no - 1) * page_size, limit=page_size, @@ -174,43 +121,17 @@ class OperationLogService: out_schema=OperationLogOutSchema, ) - @classmethod - async def detail_service(cls, auth: AuthSchema, id: int) -> OperationLogDetailOutSchema: - """ - 获取操作日志详情。 - - 参数: - - auth (AuthSchema): 认证信息模型。 - - id (int): 操作日志ID。 - - 返回: - - OperationLogDetailOutSchema: 操作日志详情。 - """ - - crud = OperationLogCRUD(auth) + async def detail(self, id: int) -> OperationLogDetailOutSchema: + crud = OperationLogCRUD(self.auth) return await crud.get_or_404(id=id, out_schema=OperationLogDetailOutSchema) - @classmethod - async def delete_service(cls, auth: AuthSchema, ids: list[int]) -> None: - """ - 删除操作日志。 - - 参数: - - auth (AuthSchema): 认证信息模型。 - - ids (list[int]): 操作日志ID列表。 - - 异常: - - CustomException: 删除失败时抛出。 - - 返回: - - None - """ + async def delete(self, ids: list[int]) -> None: if len(ids) < 1: raise CustomException(msg="删除失败,删除对象不能为空") - existing = await OperationLogCRUD(auth).list(search={"id": ("in", ids)}) + existing = await OperationLogCRUD(self.auth).list(search={"id": ("in", ids)}) existing_map = {obj.id for obj in existing} for nid in ids: if nid not in existing_map: raise CustomException(msg="删除失败,该数据不存在") - crud = OperationLogCRUD(auth) - await crud.delete(ids) + crud = OperationLogCRUD(self.auth) + await crud.delete(ids=ids) diff --git a/backend/app/api/v1/module_system/notice/controller.py b/backend/app/api/v1/module_system/notice/controller.py index 37d2118a..2f03fcb2 100644 --- a/backend/app/api/v1/module_system/notice/controller.py +++ b/backend/app/api/v1/module_system/notice/controller.py @@ -22,11 +22,10 @@ from .schema import ( ) from .service import NoticeService -NoticeRouter = APIRouter(route_class=OperationLogRoute, prefix="/notice", tags=["公告通知"]) +NoticeRouter = APIRouter(route_class=OperationLogRoute, prefix="/notice", tags=["系统管理", "公告通知"]) _NOTICE_NS = "notice" - @NoticeRouter.get( "/detail/{id}", summary="获取公告详情", @@ -36,20 +35,9 @@ async def get_notice_detail_controller( id: Annotated[int, Path(description="公告ID")], auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:notice:detail"]))], ) -> JSONResponse: - """ - 获取公告详情。 - - 参数: - - id (int): 公告ID。 - - auth (AuthSchema): 认证信息模型。 - - 返回: - - JSONResponse: 包含公告详情的响应模型。 - """ - result_dict = await NoticeService.detail_service(id=id, auth=auth) + result_dict = await NoticeService(auth).detail(id=id) return SuccessResponse(data=result_dict, msg="获取公告详情成功") - @NoticeRouter.get( "/list", summary="查询公告", @@ -60,19 +48,7 @@ async def get_notice_list_controller( search: Annotated[NoticeQueryParam, Depends()], auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:notice:query"]))], ) -> JSONResponse: - """ - 查询公告。 - - 参数: - - page (PaginationQueryParam): 分页查询参数模型。 - - search (NoticeQueryParam): 查询公告参数模型。 - - auth (AuthSchema): 认证信息模型。 - - 返回: - - JSONResponse: 包含分页公告详情的响应模型。 - """ - result_dict = await NoticeService.page_service( - auth=auth, + result_dict = await NoticeService(auth).page( page_no=page.page_no, page_size=page.page_size, search=search, @@ -80,7 +56,6 @@ async def get_notice_list_controller( ) return SuccessResponse(data=result_dict, msg="查询公告列表成功") - @NoticeRouter.post( "/create", summary="创建公告", @@ -90,21 +65,10 @@ async def create_notice_controller( data: NoticeCreateSchema, auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:notice:create"]))], ) -> JSONResponse: - """ - 创建公告。 - - 参数: - - data (NoticeCreateSchema): 创建公告负载模型。 - - auth (AuthSchema): 认证信息模型。 - - 返回: - - JSONResponse: 包含创建公告结果的响应模型。 - """ - result_dict = await NoticeService.create_service(auth=auth, data=data) + result_dict = await NoticeService(auth).create(data=data) await FastAPICache.clear(namespace=_NOTICE_NS) return SuccessResponse(data=result_dict, msg="创建公告成功") - @NoticeRouter.put( "/update/{id}", summary="修改公告", @@ -115,22 +79,10 @@ async def update_notice_controller( id: Annotated[int, Path(description="公告ID")], auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:notice:update"]))], ) -> JSONResponse: - """ - 修改公告。 - - 参数: - - data (NoticeUpdateSchema): 修改公告负载模型。 - - id (int): 公告ID。 - - auth (AuthSchema): 认证信息模型。 - - 返回: - - JSONResponse: 包含修改公告结果的响应模型。 - """ - result_dict = await NoticeService.update_service(auth=auth, id=id, data=data) + result_dict = await NoticeService(auth).update(id=id, data=data) await FastAPICache.clear(namespace=_NOTICE_NS) return SuccessResponse(data=result_dict, msg="修改公告成功") - @NoticeRouter.delete( "/delete", summary="删除公告", @@ -140,21 +92,10 @@ async def delete_notice_controller( ids: Annotated[list[int], Body(description="ID列表")], auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:notice:delete"]))], ) -> JSONResponse: - """ - 删除公告。 - - 参数: - - ids (list[int]): 公告ID列表。 - - auth (AuthSchema): 认证信息模型。 - - 返回: - - JSONResponse: 包含删除公告结果的响应模型。 - """ - await NoticeService.delete_service(auth=auth, ids=ids) + await NoticeService(auth).delete(ids=ids) await FastAPICache.clear(namespace=_NOTICE_NS) return SuccessResponse(msg="删除公告成功") - @NoticeRouter.patch( "/status/batch", summary="批量修改公告状态", @@ -164,21 +105,10 @@ async def batch_set_available_notice_controller( data: BatchSetAvailable, auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:notice:patch"]))], ) -> JSONResponse: - """ - 批量修改公告状态。 - - 参数: - - data (BatchSetAvailable): 批量修改公告状态负载模型。 - - auth (AuthSchema): 认证信息模型。 - - 返回: - - JSONResponse: 包含批量修改公告状态结果的响应模型。 - """ - await NoticeService.set_available_service(auth=auth, data=data) + await NoticeService(auth).set_available(data=data) await FastAPICache.clear(namespace=_NOTICE_NS) return SuccessResponse(msg="批量修改公告状态成功") - @NoticeRouter.post( "/export", summary="导出公告", @@ -187,19 +117,9 @@ async def export_notice_list_controller( search: Annotated[NoticeQueryParam, Depends()], auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:notice:export"]))], ) -> StreamingResponse: - """ - 导出公告。 - - 参数: - - search (NoticeQueryParam): 查询公告参数模型。 - - auth (AuthSchema): 认证信息模型。 - - 返回: - - StreamingResponse: 包含导出公告的流式响应模型。 - """ - result_dict_list = await NoticeService.list_service(search=search, auth=auth) + result_dict_list = await NoticeService(auth).list(search=search) export_data = [item.model_dump() for item in result_dict_list] - export_result = await NoticeService.export_notice_service(notice_list=export_data) + export_result = NoticeService.export(notice_list=export_data) return StreamResponse( data=bytes2file_response(export_result), @@ -207,7 +127,6 @@ async def export_notice_list_controller( headers={"Content-Disposition": "attachment; filename=notice.xlsx"}, ) - @NoticeRouter.get( "/available", summary="获取全局启用公告", @@ -217,19 +136,9 @@ async def export_notice_list_controller( async def get_notice_list_available_controller( auth: Annotated[AuthSchema, Depends(get_current_user)], ) -> JSONResponse: - """ - 获取全局启用公告。 - - 参数: - - auth (AuthSchema): 认证信息模型。 - - 返回: - - JSONResponse: 包含分页已启用公告详情的响应模型。 - """ - result_dict = await NoticeService.available_page_service(auth=auth) + result_dict = await NoticeService(auth).available_page() return SuccessResponse(data=result_dict, msg="查询已启用公告列表成功") - @NoticeRouter.get( "/panel", summary="通知面板数据(铃铛)", @@ -240,13 +149,9 @@ async def get_notification_panel_controller( auth: Annotated[AuthSchema, Depends(get_current_user)], ) -> JSONResponse: """通知面板聚合接口,返回通知、消息、待办三个列表。""" - result = await NoticeService.get_panel_data_service(auth=auth) + result = await NoticeService(auth).panel_data() return SuccessResponse(data=result, msg="获取面板数据成功") - -# ============ 已读追踪端点 ============ - - @NoticeRouter.post( "/read/{id}", summary="标记已读", @@ -257,12 +162,11 @@ async def mark_read_controller( auth: Annotated[AuthSchema, Depends(get_current_user)], ) -> JSONResponse: """标记已读。通过 `sys_notice_read` 表记录已读时间。""" - await NoticeService.mark_read_service(auth=auth, notice_id=id) + await NoticeService(auth).mark_read(notice_id=id) await FastAPICache.clear(namespace=_NOTICE_NS) logger.info(f"用户[{auth.user.id}]标记通知[{id}]已读") return SuccessResponse(msg="标记已读成功") - @NoticeRouter.post( "/read-all", summary="全部已读", @@ -272,12 +176,11 @@ async def mark_all_read_controller( auth: Annotated[AuthSchema, Depends(get_current_user)], ) -> JSONResponse: """全部标记已读。返回本次操作标记的数量。""" - count = await NoticeService.mark_all_read_service(auth=auth) + count = await NoticeService(auth).mark_all_read() await FastAPICache.clear(namespace=_NOTICE_NS) logger.info(f"用户[{auth.user.id}]全部已读, 数量={count}") return SuccessResponse(data=count, msg=f"全部标记已读成功,共标记 {count} 条") - @NoticeRouter.get( "/unread-count", summary="获取未读数量", @@ -288,5 +191,5 @@ async def get_unread_count_controller( auth: Annotated[AuthSchema, Depends(get_current_user)], ) -> JSONResponse: """获取未读通知数量。通过 LEFT JOIN 统计未读数。""" - count = await NoticeService.get_unread_count_service(auth=auth) + count = await NoticeService(auth).get_unread_count() return SuccessResponse(data=count, msg="获取未读数量成功") diff --git a/backend/app/api/v1/module_system/notice/service.py b/backend/app/api/v1/module_system/notice/service.py index 0886bd4e..b47aff32 100644 --- a/backend/app/api/v1/module_system/notice/service.py +++ b/backend/app/api/v1/module_system/notice/service.py @@ -22,65 +22,29 @@ class NoticeService: 提供公告 CRUD、状态切换、已启用公告分页查询、消息面板、Excel 导出等业务能力。 """ - @classmethod - async def detail_service(cls, auth: AuthSchema, id: int) -> NoticeOutSchema: - """ - 获取公告详情。 + def __init__(self, auth: AuthSchema) -> None: + self.auth = auth - 参数: - - auth (AuthSchema): 认证信息模型。 - - id (int): 公告ID。 + async def detail(self, id: int) -> NoticeOutSchema: + return await NoticeCRUD(self.auth).get_or_404(id=id, out_schema=NoticeOutSchema) - 返回: - - NoticeOutSchema: 公告响应模型。 - """ - return await NoticeCRUD(auth).get_or_404(id=id, out_schema=NoticeOutSchema) - - @classmethod - async def list_service( - cls, - auth: AuthSchema, + async def list( + self, search: NoticeQueryParam | None = None, order_by: list[dict] | None = None, ) -> list[NoticeOutSchema]: - """ - 获取公告列表。 - - 参数: - - auth (AuthSchema): 认证信息模型。 - - search (NoticeQueryParam | None): 查询参数模型。 - - order_by (list[dict] | None): 排序参数列表。 - - 返回: - - list[NoticeOutSchema]: 公告响应模型列表。 - """ - notice_obj_list = await NoticeCRUD(auth).list(search=vars(search) if search else None, order_by=order_by) + notice_obj_list = await NoticeCRUD(self.auth).list(search=vars(search) if search else None, order_by=order_by) return [NoticeOutSchema.model_validate(notice_obj) for notice_obj in notice_obj_list] - @classmethod - async def page_service( - cls, - auth: AuthSchema, + async def page( + self, page_no: int, page_size: int, search: NoticeQueryParam | None = None, order_by: list[dict] | None = None, ) -> dict: - """ - 分页查询公告(数据库 OFFSET/LIMIT)。 - - 参数: - - auth (AuthSchema): 认证信息模型 - - page_no (int): 页码(从 1 开始) - - page_size (int): 每页条数 - - search (NoticeQueryParam | None): 查询条件 - - order_by (list[dict] | None): 排序字段列表 - - 返回: - - dict: 分页结果(结构由 `CRUD.page` 返回约定) - """ offset = (page_no - 1) * page_size - return await NoticeCRUD(auth).page( + return await NoticeCRUD(self.auth).page( offset=offset, limit=page_size, order_by=order_by or [{"id": "asc"}], @@ -88,18 +52,8 @@ class NoticeService: out_schema=NoticeOutSchema, ) - @classmethod - async def available_page_service(cls, auth: AuthSchema) -> dict: - """ - 已启用公告分页(与历史行为一致:固定第 1 页、每页 10 条)。 - - 参数: - - auth (AuthSchema): 认证信息模型 - - 返回: - - dict: 分页结果(结构由 `CRUD.page` 返回约定) - """ - return await NoticeCRUD(auth).page( + async def available_page(self) -> dict: + return await NoticeCRUD(self.auth).page( offset=0, limit=10, order_by=[{"id": "asc"}], @@ -107,102 +61,36 @@ class NoticeService: out_schema=NoticeOutSchema, ) - @classmethod - async def create_service(cls, auth: AuthSchema, data: NoticeCreateSchema) -> NoticeOutSchema: - """ - 创建公告。 - - 参数: - - auth (AuthSchema): 认证信息模型。 - - data (NoticeCreateSchema): 创建公告负载模型。 - - 返回: - - NoticeOutSchema: 创建的公告响应模型。 - - 异常: - - CustomException: 创建失败,该公告通知已存在。 - """ - notice = await NoticeCRUD(auth).get(notice_title=data.notice_title) + async def create(self, data: NoticeCreateSchema) -> NoticeOutSchema: + notice = await NoticeCRUD(self.auth).get(notice_title=data.notice_title) if notice: raise CustomException(msg="创建失败,该数据已存在") - notice_obj = await NoticeCRUD(auth).create(data=data) + notice_obj = await NoticeCRUD(self.auth).create(data=data) return NoticeOutSchema.model_validate(notice_obj) - @classmethod - async def update_service(cls, auth: AuthSchema, id: int, data: NoticeUpdateSchema) -> NoticeOutSchema: - """ - 更新公告。 - - 参数: - - auth (AuthSchema): 认证信息模型。 - - id (int): 公告ID。 - - data (NoticeUpdateSchema): 更新公告负载模型。 - - 返回: - - NoticeOutSchema: 更新的公告响应模型。 - - 异常: - - CustomException: 更新失败,该公告通知不存在或公告通知标题重复。 - """ - _ = await NoticeCRUD(auth).get_or_404(id=id, msg="更新失败,该数据不存在") - exist_notice = await NoticeCRUD(auth).get(notice_title=data.notice_title) + async def update(self, id: int, data: NoticeUpdateSchema) -> NoticeOutSchema: + _ = await NoticeCRUD(self.auth).get_or_404(id=id, msg="更新失败,该数据不存在") + exist_notice = await NoticeCRUD(self.auth).get(notice_title=data.notice_title) if exist_notice and exist_notice.id != id: raise CustomException(msg="更新失败,标题已存在") - notice_obj = await NoticeCRUD(auth).update(id=id, data=data) + notice_obj = await NoticeCRUD(self.auth).update(id=id, data=data) return NoticeOutSchema.model_validate(notice_obj) - @classmethod - async def delete_service(cls, auth: AuthSchema, ids: list[int]) -> None: - """ - 删除公告。 - - 参数: - - auth (AuthSchema): 认证信息模型。 - - ids (list[int]): 删除的ID列表。 - - 异常: - - CustomException: 删除失败,删除对象不能为空或该公告通知不存在。 - - 返回: - - None - """ + async def delete(self, ids: list[int]) -> None: if len(ids) < 1: raise CustomException(msg="删除失败,删除对象不能为空") - notices = await NoticeCRUD(auth).list(search={"id": ("in", ids)}) + notices = await NoticeCRUD(self.auth).list(search={"id": ("in", ids)}) notice_map = {n.id: n for n in notices} for nid in ids: if nid not in notice_map: raise CustomException(msg="删除失败,该数据不存在") - await NoticeCRUD(auth).delete(ids=ids) + await NoticeCRUD(self.auth).delete(ids=ids) - @classmethod - async def set_available_service(cls, auth: AuthSchema, data: BatchSetAvailable) -> None: - """ - 批量设置公告状态。 + async def set_available(self, data: BatchSetAvailable) -> None: + await NoticeCRUD(self.auth).set(ids=data.ids, status=data.status) - 参数: - - auth (AuthSchema): 认证信息模型。 - - data (BatchSetAvailable): 批量设置可用负载模型。 - - 异常: - - CustomException: 批量设置失败,该公告通知不存在。 - - 返回: - - None - """ - await NoticeCRUD(auth).set(ids=data.ids, status=data.status) - - @classmethod - async def export_service(cls, notice_list: list[dict]) -> bytes: - """ - 导出公告列表。 - - 参数: - - notice_list (list[dict]): 公告详情字典列表。 - - 返回: - - bytes: Excel 文件的字节流。 - """ + @staticmethod + def export(notice_list: list[dict]) -> bytes: mapping_dict = { "id": "编号", "notice_title": "公告标题", @@ -215,75 +103,56 @@ class NoticeService: "created_id": "创建者ID", "updated_id": "更新者ID", } - - # 复制数据并转换状态 data = notice_list.copy() for item in data: - # 处理状态 item["status"] = "启用" if item.get("status") == 0 else "停用" - # 处理公告类型 item["notice_type"] = "通知" if item.get("notice_type") == "1" else "公告" - return ExcelUtil.export_list2excel(list_data=data, mapping_dict=mapping_dict) - @classmethod - async def latest_service(cls, auth: AuthSchema, limit: int = 5) -> list[NoticeOutSchema]: - """获取最新 N 条已启用公告""" + async def latest(self, limit: int = 5) -> list[NoticeOutSchema]: from sqlalchemy import desc, select - from .model import NoticeModel - from .schema import NoticeOutSchema - stmt = select(NoticeModel).where(NoticeModel.status == 0).order_by(desc(NoticeModel.created_time)).limit(limit) - result = await auth.db.execute(stmt) + result = await self.auth.db.execute(stmt) notices = result.scalars().all() return [NoticeOutSchema.model_validate(n) for n in notices] - # ============ 已读追踪 ============ - - @classmethod - async def mark_read_service(cls, auth: AuthSchema, notice_id: int) -> None: - """标记通知为已读(幂等:已读的不重复写入)""" + async def mark_read(self, notice_id: int) -> None: from datetime import datetime from sqlalchemy import select - # 检查公告是否存在 - notice = await NoticeCRUD(auth).get(id=notice_id) + notice = await NoticeCRUD(self.auth).get(id=notice_id) if not notice: raise CustomException(msg="该公告不存在") - # 检查是否已读 exist_stmt = select(NoticeReadModel).where( - NoticeReadModel.user_id == auth.user.id, + NoticeReadModel.user_id == self.auth.user.id, NoticeReadModel.notice_id == notice_id, ) - result = await auth.db.execute(exist_stmt) + result = await self.auth.db.execute(exist_stmt) if result.scalar_one_or_none(): - return # 已读,幂等返回 + return read_record = NoticeReadModel( - user_id=auth.user.id, + user_id=self.auth.user.id, notice_id=notice_id, read_time=datetime.now(), ) - auth.db.add(read_record) - await auth.db.flush() + self.auth.db.add(read_record) + await self.auth.db.flush() - @classmethod - async def mark_all_read_service(cls, auth: AuthSchema) -> int: - """全部标记已读:将当前租户所有 status=0 且未读的公告批量标记为已读,返回操作数量""" + async def mark_all_read(self) -> int: from datetime import datetime - from sqlalchemy import select + from sqlalchemy import insert, select - # 查询所有已启用但未读的公告 - read_ids_stmt = select(NoticeReadModel.notice_id).where(NoticeReadModel.user_id == auth.user.id) - read_ids_result = await auth.db.execute(read_ids_stmt) + read_ids_stmt = select(NoticeReadModel.notice_id).where(NoticeReadModel.user_id == self.auth.user.id) + read_ids_result = await self.auth.db.execute(read_ids_stmt) read_ids = {row[0] for row in read_ids_result.fetchall()} notices_stmt = select(NoticeModel.id).where(NoticeModel.status == 0) - notices_result = await auth.db.execute(notices_stmt) + notices_result = await self.auth.db.execute(notices_stmt) all_ids = {row[0] for row in notices_result.fetchall()} unread_ids = all_ids - read_ids @@ -291,48 +160,38 @@ class NoticeService: return 0 now = datetime.now() - from sqlalchemy import insert - if unread_ids: - await auth.db.execute( + await self.auth.db.execute( insert(NoticeReadModel), - [{"user_id": auth.user.id, "notice_id": nid, "read_time": now} for nid in unread_ids], + [{"user_id": self.auth.user.id, "notice_id": nid, "read_time": now} for nid in unread_ids], ) - await auth.db.flush() + await self.auth.db.flush() return len(unread_ids) - @classmethod - async def get_unread_count_service(cls, auth: AuthSchema) -> int: - """获取当前用户未读通知数量""" + async def get_unread_count(self) -> int: from sqlalchemy import func, select - # 所有已启用公告总数 total_stmt = select(func.count()).select_from(NoticeModel).where(NoticeModel.status == 0) - total_result = await auth.db.execute(total_stmt) + total_result = await self.auth.db.execute(total_stmt) total_count = total_result.scalar() or 0 - # 已读数量 - read_stmt = select(func.count()).select_from(NoticeReadModel).where(NoticeReadModel.user_id == auth.user.id) - read_result = await auth.db.execute(read_stmt) + read_stmt = select(func.count()).select_from(NoticeReadModel).where(NoticeReadModel.user_id == self.auth.user.id) + read_result = await self.auth.db.execute(read_stmt) read_count = read_result.scalar() or 0 return max(0, total_count - read_count) - @classmethod - async def panel_data_service(cls, auth: AuthSchema) -> PanelDataOut: - """聚合通知面板数据:通知 + 消息 + 待办""" + async def panel_data(self) -> PanelDataOut: from sqlalchemy import desc, select - # 1. 通知:最新 5 条已启用公告 - notices = await cls.latest_service(auth, limit=5) + notices = await self.latest(limit=5) - # 2. 消息:最近的操作日志(作为系统消息) messages = [] try: from app.api.v1.module_system.log.model import OperationLogModel stmt = select(OperationLogModel).order_by(desc(OperationLogModel.created_time)).limit(5) - result = await auth.db.execute(stmt) + result = await self.auth.db.execute(stmt) logs = result.scalars().all() for log_entry in logs: messages.append( @@ -347,7 +206,6 @@ class NoticeService: except Exception as e: logger.warning(f"获取面板消息数据失败(操作日志表可能不存在),已跳过: {e}") - # 3. 待办:暂无数据源,返回空列表 pendings: list[dict] = [] return PanelDataOut( diff --git a/backend/app/api/v1/module_system/params/controller.py b/backend/app/api/v1/module_system/params/controller.py index cc974e1a..ef3d4654 100644 --- a/backend/app/api/v1/module_system/params/controller.py +++ b/backend/app/api/v1/module_system/params/controller.py @@ -14,8 +14,7 @@ from app.utils.common_util import bytes2file_response from .schema import ParamsCreateSchema, ParamsOutSchema, ParamsQueryParam, ParamsUpdateSchema from .service import ParamsService -ParamsRouter = APIRouter(route_class=OperationLogRoute, prefix="/param", tags=["参数管理"]) - +ParamsRouter = APIRouter(route_class=OperationLogRoute, prefix="/param", tags=["系统管理", "参数管理"]) @ParamsRouter.get( "/detail/{id}", @@ -26,20 +25,9 @@ async def get_param_detail_controller( id: Annotated[int, Path(description="参数ID")], auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:param:detail"]))], ) -> JSONResponse: - """ - 获取参数详情 - - 参数: - - id (int): 参数ID - - auth (AuthSchema): 认证信息模型 - - 返回: - - JSONResponse: 包含参数详情的 JSON 响应 - """ - result_dict = await ParamsService.detail_service(id=id, auth=auth) + result_dict = await ParamsService(auth).detail(id=id) return SuccessResponse(data=result_dict, msg="获取参数详情成功") - @ParamsRouter.get( "/key/{config_key}", summary="根据配置键获取参数详情", @@ -49,20 +37,9 @@ async def get_param_by_key_controller( config_key: Annotated[str, Path(description="配置键")], auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:param:query"]))], ) -> JSONResponse: - """ - 根据配置键获取参数详情 - - 参数: - - config_key (str): 配置键 - - auth (AuthSchema): 认证信息模型 - - 返回: - - JSONResponse: 包含参数详情的 JSON 响应 - """ - result_dict = await ParamsService.get_by_key_service(config_key=config_key, auth=auth) + result_dict = await ParamsService(auth).get_by_key(config_key=config_key) return SuccessResponse(data=result_dict, msg="根据配置键获取参数详情成功") - @ParamsRouter.get( "/value/{config_key}", summary="根据配置键获取参数值", @@ -72,20 +49,9 @@ async def get_config_value_by_key_controller( config_key: Annotated[str, Path(description="配置键")], auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:param:query"]))], ) -> JSONResponse: - """ - 根据配置键获取参数值 - - 参数: - - config_key (str): 配置键 - - auth (AuthSchema): 认证信息模型 - - 返回: - - JSONResponse: 包含参数值的 JSON 响应 - """ - result_value = await ParamsService.get_config_value_by_key_service(config_key=config_key, auth=auth) + result_value = await ParamsService(auth).get_config_value_by_key(config_key=config_key) return SuccessResponse(data=result_value, msg="根据配置键获取参数值成功") - @ParamsRouter.get( "/list", summary="获取参数列表", @@ -96,19 +62,7 @@ async def get_param_list_controller( page: Annotated[PaginationQueryParam, Depends()], search: Annotated[ParamsQueryParam, Depends()], ) -> JSONResponse: - """ - 获取参数列表 - - 参数: - - auth (AuthSchema): 认证信息模型 - - page (PaginationQueryParam): 分页查询参数模型 - - search (ParamsQueryParam): 参数查询参数模型 - - 返回: - - JSONResponse: 包含参数列表的 JSON 响应 - """ - result_dict = await ParamsService.page_service( - auth=auth, + result_dict = await ParamsService(auth).page( page_no=page.page_no, page_size=page.page_size, search=search, @@ -116,7 +70,6 @@ async def get_param_list_controller( ) return SuccessResponse(data=result_dict, msg="查询参数列表成功") - @ParamsRouter.post( "/create", summary="创建参数", @@ -127,21 +80,9 @@ async def create_param_controller( redis: Annotated[Redis, Depends(redis_getter)], auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:param:create"]))], ) -> JSONResponse: - """ - 创建参数 - - 参数: - - data (ParamsCreateSchema): 参数创建模型 - - redis (Redis): Redis 客户端实例 - - auth (AuthSchema): 认证信息模型 - - 返回: - - JSONResponse: 包含创建参数结果的 JSON 响应 - """ - result_dict = await ParamsService.create_service(auth=auth, redis=redis, data=data) + result_dict = await ParamsService(auth).create(redis=redis, data=data) return SuccessResponse(data=result_dict, msg="创建参数成功") - @ParamsRouter.put( "/update/{id}", summary="修改参数", @@ -153,22 +94,9 @@ async def update_param_controller( redis: Annotated[Redis, Depends(redis_getter)], auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:param:update"]))], ) -> JSONResponse: - """ - 修改参数 - - 参数: - - data (ParamsUpdateSchema): 参数更新模型 - - id (int): 参数ID - - redis (Redis): Redis 客户端实例 - - auth (AuthSchema): 认证信息模型 - - 返回: - - JSONResponse: 包含修改参数结果的 JSON 响应 - """ - result_dict = await ParamsService.update_service(auth=auth, redis=redis, id=id, data=data) + result_dict = await ParamsService(auth).update(redis=redis, id=id, data=data) return SuccessResponse(data=result_dict, msg="更新参数成功") - @ParamsRouter.delete( "/delete", summary="删除参数", @@ -179,21 +107,9 @@ async def delete_param_controller( ids: Annotated[list[int], Body(description="ID列表")], auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:param:delete"]))], ) -> JSONResponse: - """ - 删除参数 - - 参数: - - redis (Redis): Redis 客户端实例 - - ids (list[int]): 参数ID列表 - - auth (AuthSchema): 认证信息模型 - - 返回: - - JSONResponse: 包含删除参数结果的 JSON 响应 - """ - await ParamsService.delete_service(auth=auth, redis=redis, ids=ids) + await ParamsService(auth).delete(redis=redis, ids=ids) return SuccessResponse(msg="删除参数成功") - @ParamsRouter.patch( "/status/batch", summary="批量设置参数状态", @@ -204,21 +120,9 @@ async def batch_set_status_controller( status: Annotated[str, Body(description="状态值")], auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:param:patch"]))], ) -> JSONResponse: - """ - 批量设置参数状态 - - 参数: - - ids (list[int]): 参数ID列表 - - status (str): 状态值 - - auth (AuthSchema): 认证信息模型 - - 返回: - - JSONResponse: 包含批量设置参数状态结果的 JSON 响应 - """ - await ParamsService.batch_set_status_service(auth=auth, ids=ids, status=status) + await ParamsService(auth).batch_set_status(ids=ids, status=status) return SuccessResponse(msg="批量设置参数状态成功") - @ParamsRouter.get( "/export", summary="导出参数", @@ -228,19 +132,9 @@ async def export_param_list_controller( search: Annotated[ParamsQueryParam, Depends()], auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:param:export"]))], ) -> StreamingResponse: - """ - 导出参数 - - 参数: - - search (ParamsQueryParam): 参数查询参数模型 - - auth (AuthSchema): 认证信息模型 - - 返回: - - StreamingResponse: 包含导出参数的 Excel 文件流响应 - """ - result_dict_list = await ParamsService.list_service(search=search, auth=auth) + result_dict_list = await ParamsService(auth).list(search=search) export_data = [item.model_dump() for item in result_dict_list] - export_result = await ParamsService.export_service(data_list=export_data) + export_result = ParamsService.export(data_list=export_data) return StreamResponse( data=bytes2file_response(export_result), @@ -248,7 +142,6 @@ async def export_param_list_controller( headers={"Content-Disposition": "attachment; filename=params.xlsx"}, ) - @ParamsRouter.get( "/info", summary="获取初始化缓存参数", @@ -257,14 +150,5 @@ async def export_param_list_controller( async def get_init_config_controller( redis: Annotated[Redis, Depends(redis_getter)], ) -> JSONResponse: - """ - 获取初始化缓存参数 - - 参数: - - redis (Redis): Redis 客户端实例 - - 返回: - - JSONResponse: 获取初始化缓存参数的 JSON 响应 - """ - result_dict = await ParamsService.get_init_cache_service(redis=redis, tenant_id=1) + result_dict = await ParamsService.get_init_cache(redis=redis, tenant_id=1) return SuccessResponse(data=result_dict, msg="获取初始化缓存参数成功") diff --git a/backend/app/api/v1/module_system/params/service.py b/backend/app/api/v1/module_system/params/service.py index 37eb6ef1..23a5bf46 100644 --- a/backend/app/api/v1/module_system/params/service.py +++ b/backend/app/api/v1/module_system/params/service.py @@ -28,61 +28,58 @@ class ParamsService: """ 参数管理服务 - 提供参数 CRUD、Redis 缓存同步、初始化配置、批量启/禁用、Excel 导出等业务能力。 + 设计:实例方法承载「当前用户上下文 (auth)」,``redis`` 仍是方法参数 + (因为不是每个端点都用到)。调用方写法由 + ``ParamsService.method_service(auth=...)`` 改为 ``ParamsService(auth).method(...)``。 """ - @classmethod - async def detail_service(cls, auth: AuthSchema, id: int) -> ParamsOutSchema: + def __init__(self, auth: AuthSchema) -> None: + self.auth = auth + + async def detail(self, id: int) -> ParamsOutSchema: """ 获取参数详情 参数: - - auth (AuthSchema): 认证信息模型 - id (int): 参数ID 返回: - ParamsOutSchema: 参数响应模型 """ - return await ParamsCRUD(auth).get_or_404(id=id, out_schema=ParamsOutSchema) + return await ParamsCRUD(self.auth).get_or_404(id=id, out_schema=ParamsOutSchema) - @classmethod - async def get_by_key_service(cls, auth: AuthSchema, config_key: str) -> ParamsOutSchema: + async def get_by_key(self, config_key: str) -> ParamsOutSchema: """ 根据配置键获取参数详情 参数: - - auth (AuthSchema): 认证信息模型 - config_key (str): 参数键名 返回: - ParamsOutSchema: 参数响应模型 """ - obj = await ParamsCRUD(auth).get(config_key=config_key) + obj = await ParamsCRUD(self.auth).get(config_key=config_key) if not obj: raise CustomException(msg="该数据不存在") return ParamsOutSchema.model_validate(obj) - @classmethod - async def get_config_value_by_key_service(cls, auth: AuthSchema, config_key: str) -> str | None: + async def get_config_value_by_key(self, config_key: str) -> str | None: """ 根据配置键获取参数值 参数: - - auth (AuthSchema): 认证信息模型 - config_key (str): 参数键名 返回: - str | None: 参数键值字符串或 None """ - obj = await ParamsCRUD(auth).get(config_key=config_key) + obj = await ParamsCRUD(self.auth).get(config_key=config_key) if not obj: raise CustomException(msg="该数据不存在") return obj.config_value - @classmethod - async def list_service( - cls, - auth: AuthSchema, + async def list( + self, search: ParamsQueryParam | None = None, order_by: list[dict] | None = None, ) -> list[ParamsOutSchema]: @@ -90,20 +87,17 @@ class ParamsService: 获取配置管理型列表 参数: - - auth (AuthSchema): 认证信息模型 - search (ParamsQueryParam | None): 查询参数对象 - order_by (list[dict] | None): 排序参数列表 返回: - list[ParamsOutSchema]: 参数响应模型列表 """ - obj_list = await ParamsCRUD(auth).list(search=vars(search) if search else None, order_by=order_by) + obj_list = await ParamsCRUD(self.auth).list(search=vars(search) if search else None, order_by=order_by) return [ParamsOutSchema.model_validate(obj) for obj in obj_list] - @classmethod - async def page_service( - cls, - auth: AuthSchema, + async def page( + self, page_no: int, page_size: int, search: ParamsQueryParam | None = None, @@ -113,17 +107,16 @@ class ParamsService: 分页查询系统参数(数据库 OFFSET/LIMIT)。 参数: - - auth (AuthSchema): 认证信息模型 - page_no (int): 页码(从 1 开始) - page_size (int): 每页条数 - search (ParamsQueryParam | None): 查询条件 - order_by (list[dict[str, str]] | None): 排序字段列表 返回: - - dict: 分页结果(结构由 `CRUD.page` 返回约定) + - dict: 分页结果(结构由 ``CRUD.page`` 返回约定) """ offset = (page_no - 1) * page_size - return await ParamsCRUD(auth).page( + return await ParamsCRUD(self.auth).page( offset=offset, limit=page_size, order_by=order_by or [{"id": "asc"}], @@ -131,28 +124,26 @@ class ParamsService: out_schema=ParamsOutSchema, ) - @classmethod - async def create_service(cls, auth: AuthSchema, redis: Redis, data: ParamsCreateSchema) -> ParamsOutSchema: + async def create(self, redis: Redis, data: ParamsCreateSchema) -> ParamsOutSchema: """ 创建配置管理型 参数: - - auth (AuthSchema): 认证信息模型 - redis (Redis): Redis 客户端实例 - data (ParamsCreateSchema): 配置管理型创建模型 返回: - ParamsOutSchema: 新创建的参数响应模型 """ - exist_obj = await ParamsCRUD(auth).get(config_key=data.config_key) + exist_obj = await ParamsCRUD(self.auth).get(config_key=data.config_key) if exist_obj: raise CustomException(msg="创建失败,该数据已存在") - obj = await ParamsCRUD(auth).create(data=data) + obj = await ParamsCRUD(self.auth).create(data=data) out = ParamsOutSchema.model_validate(obj) # 同步redis - redis_key = f"{RedisInitKeyConfig.SYSTEM_CONFIG.key}:{auth.user.tenant_id}:{data.config_key}" + redis_key = f"{RedisInitKeyConfig.SYSTEM_CONFIG.key}:{self.auth.user.tenant_id}:{data.config_key}" try: redis_payload = out.model_dump(mode="json") value = json.dumps(redis_payload, ensure_ascii=False) @@ -170,13 +161,11 @@ class ParamsService: return out - @classmethod - async def update_service(cls, auth: AuthSchema, redis: Redis, id: int, data: ParamsUpdateSchema) -> ParamsOutSchema: + async def update(self, redis: Redis, id: int, data: ParamsUpdateSchema) -> ParamsOutSchema: """ 更新参数 参数: - - auth (AuthSchema): 认证信息模型 - redis (Redis): Redis 客户端实例 - id (int): 参数ID - data (ParamsUpdateSchema): 参数更新模型 @@ -184,18 +173,18 @@ class ParamsService: 返回: - ParamsOutSchema: 更新后的参数响应模型 """ - exist_obj = await ParamsCRUD(auth).get_or_404(id=id, msg="更新失败,该数据不存在") + exist_obj = await ParamsCRUD(self.auth).get_or_404(id=id, msg="更新失败,该数据不存在") if exist_obj.config_key != data.config_key: raise CustomException(msg="更新失败,系统配置key不允许修改") - new_obj = await ParamsCRUD(auth).update(id=id, data=data) + new_obj = await ParamsCRUD(self.auth).update(id=id, data=data) if not new_obj: raise CustomException(msg="更新失败,系统配置不存在") out = ParamsOutSchema.model_validate(new_obj) redis_payload = out.model_dump(mode="json") # 同步redis - redis_key = f"{RedisInitKeyConfig.SYSTEM_CONFIG.key}:{auth.user.tenant_id}:{new_obj.config_key}" + redis_key = f"{RedisInitKeyConfig.SYSTEM_CONFIG.key}:{self.auth.user.tenant_id}:{new_obj.config_key}" try: value = json.dumps(redis_payload, ensure_ascii=False) result = await RedisCURD(redis).set( @@ -212,13 +201,11 @@ class ParamsService: return out - @classmethod - async def delete_service(cls, auth: AuthSchema, redis: Redis, ids: list[int]) -> None: + async def delete(self, redis: Redis, ids: list[int]) -> None: """ 删除配置管理型 参数: - - auth (AuthSchema): 认证信息模型 - redis (Redis): Redis 客户端实例 - ids (list[int]): 配置管理型ID列表 @@ -228,7 +215,7 @@ class ParamsService: if len(ids) < 1: raise CustomException(msg="删除失败,删除对象不能为空") # 批量校验参数存在性 - objs = await ParamsCRUD(auth).list(search={"id": ("in", ids)}) + objs = await ParamsCRUD(self.auth).list(search={"id": ("in", ids)}) obj_map = {o.id: o for o in objs} for pid in ids: obj = obj_map.get(pid) @@ -237,24 +224,22 @@ class ParamsService: if obj.config_type: raise CustomException(msg=f"{obj.config_name} 删除失败,系统初始化配置不可以删除") - await ParamsCRUD(auth).delete(ids=ids) + await ParamsCRUD(self.auth).delete(ids=ids) # 同步删除Redis缓存(使用删除前已获取的对象信息) for obj in objs: - redis_key = f"{RedisInitKeyConfig.SYSTEM_CONFIG.key}:{auth.user.tenant_id}:{obj.config_key}" + redis_key = f"{RedisInitKeyConfig.SYSTEM_CONFIG.key}:{self.auth.user.tenant_id}:{obj.config_key}" try: await RedisCURD(redis).delete(redis_key) except Exception as e: logger.error(f"删除系统配置失败: {e}") raise CustomException(msg="同步删除缓存失败") from e - @classmethod - async def batch_set_status_service(cls, auth: AuthSchema, ids: list[int], status: str) -> None: + async def batch_set_status(self, ids: list[int], status: str) -> None: """ 批量设置系统参数状态 参数: - - auth (AuthSchema): 认证信息模型 - ids (list[int]): 系统参数ID列表 - status (str): 状态值 @@ -264,12 +249,12 @@ class ParamsService: if not ids: raise CustomException(msg="请选择要操作的数据") - await ParamsCRUD(auth).set(ids=ids, status=status) + await ParamsCRUD(self.auth).set(ids=ids, status=status) - @classmethod - async def export_service(cls, data_list: list[dict]) -> bytes: + @staticmethod + def export(data_list: list[dict]) -> bytes: """ - 导出参数列表 + 导出参数列表(无状态工具方法) 参数: - data_list (list[dict]): 参数字典列表 @@ -298,10 +283,10 @@ class ParamsService: return ExcelUtil.export_list2excel(list_data=data, mapping_dict=mapping_dict) - @classmethod - async def init_cache_service(cls, redis: Redis) -> None: + @staticmethod + async def init_cache(redis: Redis) -> None: """ - 初始化系统配置并按租户缓存。 + 初始化系统参数并按租户缓存(无 auth)。 参数: - redis (Redis): Redis 客户端实例 @@ -311,8 +296,8 @@ class ParamsService: """ async with async_db_session() as session: async with session.begin(): - auth = AuthSchema(db=session, check_data_scope=False) - config_obj = await ParamsCRUD(auth).list() + init_auth = AuthSchema(db=session, check_data_scope=False) + config_obj = await ParamsCRUD(init_auth).list() if not config_obj: raise CustomException(msg="该数据不存在") try: @@ -334,10 +319,10 @@ class ParamsService: logger.error(f"❌️ 初始化系统配置失败: {e}") raise CustomException(msg="初始化系统配置失败") from e - @classmethod - async def get_init_cache_service(cls, redis: Redis, tenant_id: int = 1) -> list[dict]: + @staticmethod + async def get_init_cache(redis: Redis, tenant_id: int = 1) -> list[dict]: """ - 获取系统配置 + 获取系统配置(无 auth)。 参数: - redis (Redis): Redis 客户端实例 @@ -363,10 +348,8 @@ class ParamsService: if not configs: async with async_db_session() as session: async with session.begin(): - from app.core.base_schema import AuthSchema - - auth = AuthSchema(db=session, check_data_scope=False) - config_obj = await ParamsCRUD(auth).list() + init_auth = AuthSchema(db=session, check_data_scope=False) + config_obj = await ParamsCRUD(init_auth).list() if config_obj: try: for config in config_obj: @@ -388,8 +371,8 @@ class ParamsService: return configs - @classmethod - async def get_system_config_for_middleware(cls, redis: Redis) -> dict: + @staticmethod + async def get_system_config_for_middleware(redis: Redis) -> dict: """ 获取中间件所需的系统配置(带 60 秒内存缓存,避免每请求查 Redis)。 @@ -403,13 +386,13 @@ class ParamsService: if _mid_config_cache["data"] and now - _mid_config_cache["ts"] < _MID_CONFIG_TTL: return _mid_config_cache["data"] - config_result = await cls._fetch_system_config_for_middleware(redis) + config_result = await ParamsService._fetch_system_config_for_middleware(redis) _mid_config_cache["data"] = config_result _mid_config_cache["ts"] = now return config_result - @classmethod - async def _fetch_system_config_for_middleware(cls, redis: Redis) -> dict: + @staticmethod + async def _fetch_system_config_for_middleware(redis: Redis) -> dict: # 定义需要获取的配置键 config_keys = [ f"{RedisInitKeyConfig.SYSTEM_CONFIG.key}:1:demo_enable", @@ -433,7 +416,9 @@ class ParamsService: if config_values[0]: try: demo_config = json.loads(config_values[0]) - config_result["demo_enable"] = demo_config.get("config_value", False) if isinstance(demo_config, dict) else False + config_result["demo_enable"] = ( + demo_config.get("config_value", False) if isinstance(demo_config, dict) else False + ) except json.JSONDecodeError: logger.error("解析演示模式配置失败") diff --git a/backend/app/api/v1/module_system/position/controller.py b/backend/app/api/v1/module_system/position/controller.py index 9923dd0a..25735913 100644 --- a/backend/app/api/v1/module_system/position/controller.py +++ b/backend/app/api/v1/module_system/position/controller.py @@ -20,11 +20,10 @@ from .schema import ( ) from .service import PositionService -PositionRouter = APIRouter(route_class=OperationLogRoute, prefix="/position", tags=["岗位管理"]) +PositionRouter = APIRouter(route_class=OperationLogRoute, prefix="/position", tags=["系统管理", "岗位管理"]) _POS_NS = "position" - @PositionRouter.get( "/list", summary="查询岗位", @@ -36,22 +35,10 @@ async def get_obj_list_controller( search: Annotated[PositionQueryParam, Depends()], auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:position:query"]))], ) -> JSONResponse: - """ - 查询岗位列表 - - 参数: - - page (PaginationQueryParam): 分页查询参数 - - search (PositionQueryParam): 查询参数 - - auth (AuthSchema): 认证信息模型 - - 返回: - - JSONResponse: 分页查询结果 - """ order_by = [{"order": "asc"}] if page.order_by: order_by = page.order_by - result_dict = await PositionService.page_service( - auth=auth, + result_dict = await PositionService(auth).page( page_no=page.page_no, page_size=page.page_size, search=search, @@ -59,7 +46,6 @@ async def get_obj_list_controller( ) return SuccessResponse(data=result_dict, msg="查询岗位列表成功") - @PositionRouter.get( "/detail/{id}", summary="查询岗位详情", @@ -69,20 +55,9 @@ async def get_obj_detail_controller( id: Annotated[int, Path(description="岗位ID")], auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:position:detail"]))], ) -> JSONResponse: - """ - 查询岗位详情 - - 参数: - - id (int): 岗位ID - - auth (AuthSchema): 认证信息模型 - - 返回: - - JSONResponse: 岗位详情对象 - """ - result_dict = await PositionService.detail_service(id=id, auth=auth) + result_dict = await PositionService(auth).detail(id=id) return SuccessResponse(data=result_dict, msg="获取岗位详情成功") - @PositionRouter.post( "/create", summary="创建岗位", @@ -92,21 +67,10 @@ async def create_obj_controller( data: PositionCreateSchema, auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:position:create"]))], ) -> JSONResponse: - """ - 创建岗位 - - 参数: - - data (PositionCreateSchema): 创建岗位模型 - - auth (AuthSchema): 认证信息模型 - - 返回: - - JSONResponse: 岗位详情对象 - """ - result_dict = await PositionService.create_service(data=data, auth=auth) + result_dict = await PositionService(auth).create(data=data) await FastAPICache.clear(namespace=_POS_NS) return SuccessResponse(data=result_dict, msg="创建岗位成功") - @PositionRouter.put( "/update/{id}", summary="修改岗位", @@ -117,22 +81,10 @@ async def update_obj_controller( id: Annotated[int, Path(description="岗位ID")], auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:position:update"]))], ) -> JSONResponse: - """ - 修改岗位 - - 参数: - - data (PositionUpdateSchema): 修改岗位模型 - - id (int): 岗位ID - - auth (AuthSchema): 认证信息模型 - - 返回: - - JSONResponse: 岗位详情对象 - """ - result_dict = await PositionService.update_service(id=id, data=data, auth=auth) + result_dict = await PositionService(auth).update(id=id, data=data) await FastAPICache.clear(namespace=_POS_NS) return SuccessResponse(data=result_dict, msg="修改岗位成功") - @PositionRouter.delete( "/delete", summary="删除岗位", @@ -142,21 +94,10 @@ async def delete_obj_controller( ids: Annotated[list[int], Body(description="ID列表")], auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:position:delete"]))], ) -> JSONResponse: - """ - 删除岗位 - - 参数: - - ids (list[int]): ID列表 - - auth (AuthSchema): 认证信息模型 - - 返回: - - JSONResponse: 成功消息 - """ - await PositionService.delete_service(ids=ids, auth=auth) + await PositionService(auth).delete(ids=ids) await FastAPICache.clear(namespace=_POS_NS) return SuccessResponse(msg="删除岗位成功") - @PositionRouter.patch( "/status/batch", summary="批量修改岗位状态", @@ -166,21 +107,10 @@ async def batch_set_available_obj_controller( data: BatchSetAvailable, auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:position:patch"]))], ) -> JSONResponse: - """ - 批量修改岗位状态 - - 参数: - - data (BatchSetAvailable): 批量修改岗位状态模型 - - auth (AuthSchema): 认证信息模型 - - 返回: - - JSONResponse: 成功消息 - """ - await PositionService.set_available_service(data=data, auth=auth) + await PositionService(auth).set_available(data=data) await FastAPICache.clear(namespace=_POS_NS) return SuccessResponse(msg="批量修改岗位状态成功") - @PositionRouter.get( "/export", summary="导出岗位", @@ -190,18 +120,8 @@ async def export_obj_list_controller( search: Annotated[PositionQueryParam, Depends()], auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:position:export"]))], ) -> StreamingResponse: - """ - 导出岗位 - - 参数: - - search (PositionQueryParam): 查询参数 - - auth (AuthSchema): 认证信息模型 - - 返回: - - StreamingResponse: 岗位Excel文件流 - """ - position_query_result = await PositionService.list_service(search=search, auth=auth) - position_export_result = await PositionService.export_list_service(position_list=position_query_result) + position_query_result = await PositionService(auth).list(search=search) + position_export_result = PositionService.export_list(position_list=position_query_result) return StreamResponse( data=bytes2file_response(position_export_result), diff --git a/backend/app/api/v1/module_system/position/service.py b/backend/app/api/v1/module_system/position/service.py index 72d3ec08..3cda1c7e 100644 --- a/backend/app/api/v1/module_system/position/service.py +++ b/backend/app/api/v1/module_system/position/service.py @@ -18,65 +18,29 @@ class PositionService: 提供岗位 CRUD、批量启/禁用、Excel 导出等业务能力。 """ - @classmethod - async def detail_service(cls, auth: AuthSchema, id: int) -> PositionOutSchema: - """ - 获取岗位详情 + def __init__(self, auth: AuthSchema) -> None: + self.auth = auth - 参数: - - auth (AuthSchema): 认证信息模型 - - id (int): 岗位ID + async def detail(self, id: int) -> PositionOutSchema: + return await PositionCRUD(self.auth).get_or_404(id=id, out_schema=PositionOutSchema) - 返回: - - PositionOutSchema: 岗位详情响应模型 - """ - return await PositionCRUD(auth).get_or_404(id=id, out_schema=PositionOutSchema) - - @classmethod - async def list_service( - cls, - auth: AuthSchema, + async def list( + self, search: PositionQueryParam | None = None, order_by: list[dict] | None = None, ) -> list[PositionOutSchema]: - """ - 获取岗位列表 - - 参数: - - auth (AuthSchema): 认证信息模型 - - search (PositionQueryParam | None): 查询参数对象 - - order_by (list[dict] | None): 排序参数列表 - - 返回: - - list[PositionOutSchema]: 岗位列表 - """ - position_list = await PositionCRUD(auth).list(search=vars(search) if search else None, order_by=order_by) + position_list = await PositionCRUD(self.auth).list(search=vars(search) if search else None, order_by=order_by) return [PositionOutSchema.model_validate(position) for position in position_list] - @classmethod - async def page_service( - cls, - auth: AuthSchema, + async def page( + self, page_no: int, page_size: int, search: PositionQueryParam | None = None, order_by: list[dict[str, str]] | None = None, ) -> dict: - """ - 分页查询岗位(数据库 OFFSET/LIMIT)。 - - 参数: - - auth (AuthSchema): 认证信息模型 - - page_no (int): 页码(从 1 开始) - - page_size (int): 每页条数 - - search (PositionQueryParam | None): 查询条件 - - order_by (list[dict[str, str]] | None): 排序字段列表 - - 返回: - - dict: 分页结果(结构由 `CRUD.page` 返回约定) - """ offset = (page_no - 1) * page_size - return await PositionCRUD(auth).page( + return await PositionCRUD(self.auth).page( offset=offset, limit=page_size, order_by=order_by or [{"id": "asc"}], @@ -84,96 +48,41 @@ class PositionService: out_schema=PositionOutSchema, ) - @classmethod - async def create_service(cls, auth: AuthSchema, data: PositionCreateSchema) -> PositionOutSchema: - """ - 创建岗位 - - 参数: - - auth (AuthSchema): 认证信息模型 - - data (PositionCreateSchema): 岗位创建模型 - - 返回: - - PositionOutSchema: 新创建的岗位响应模型 - """ - position = await PositionCRUD(auth).get(name=data.name) + async def create(self, data: PositionCreateSchema) -> PositionOutSchema: + position = await PositionCRUD(self.auth).get(name=data.name) if position: raise CustomException(msg="创建失败,该数据已存在") - new_position = await PositionCRUD(auth).create(data=data) + new_position = await PositionCRUD(self.auth).create(data=data) return PositionOutSchema.model_validate(new_position) - @classmethod - async def update_service(cls, auth: AuthSchema, id: int, data: PositionUpdateSchema) -> PositionOutSchema: - """ - 更新岗位 - - 参数: - - auth (AuthSchema): 认证信息模型 - - id (int): 岗位ID - - data (PositionUpdateSchema): 岗位更新模型 - - 返回: - - PositionOutSchema: 更新后的岗位响应模型 - """ - _ = await PositionCRUD(auth).get_or_404(id=id, msg="更新失败,该数据不存在") - exist_position = await PositionCRUD(auth).get(name=data.name) + async def update(self, id: int, data: PositionUpdateSchema) -> PositionOutSchema: + _ = await PositionCRUD(self.auth).get_or_404(id=id, msg="更新失败,该数据不存在") + exist_position = await PositionCRUD(self.auth).get(name=data.name) if exist_position and exist_position.id != id: raise CustomException(msg="更新失败,名称已存在") - updated_position = await PositionCRUD(auth).update(id=id, data=data) + updated_position = await PositionCRUD(self.auth).update(id=id, data=data) return PositionOutSchema.model_validate(updated_position) - @classmethod - async def delete_service(cls, auth: AuthSchema, ids: list[int]) -> None: - """ - 删除岗位 - - 参数: - - auth (AuthSchema): 认证信息模型 - - ids (list[int]): 岗位ID列表 - - 返回: - - None - """ + async def delete(self, ids: list[int]) -> None: if len(ids) < 1: raise CustomException(msg="删除失败,删除对象不能为空") - # 批量校验岗位存在性 - positions = await PositionCRUD(auth).list(search={"id": ("in", ids)}) + positions = await PositionCRUD(self.auth).list(search={"id": ("in", ids)}) position_map = {p.id: p for p in positions} for pid in ids: if pid not in position_map: raise CustomException(msg="删除失败,该数据不存在") - await PositionCRUD(auth).delete(ids=ids) + await PositionCRUD(self.auth).delete(ids=ids) - @classmethod - async def set_available_service(cls, auth: AuthSchema, data: BatchSetAvailable) -> None: - """ - 设置岗位状态 - - 参数: - - auth (AuthSchema): 认证信息模型 - - data (BatchSetAvailable): 批量设置状态模型 - - 返回: - - None - """ - positions = await PositionCRUD(auth).list(search={"id": ("in", data.ids)}) + async def set_available(self, data: BatchSetAvailable) -> None: + positions = await PositionCRUD(self.auth).list(search={"id": ("in", data.ids)}) position_map = {p.id: p for p in positions} for pid in data.ids: if pid not in position_map: raise CustomException(msg="该数据不存在") - await PositionCRUD(auth).set(ids=data.ids, status=data.status) + await PositionCRUD(self.auth).set(ids=data.ids, status=data.status) - @classmethod - async def export_list_service(cls, position_list: list[dict]) -> bytes: - """ - 导出岗位列表 - - 参数: - - position_list (list[dict]): 岗位列表对象 - - 返回: - - bytes: 导出的Excel文件字节流 - """ + @staticmethod + def export_list(position_list: list[dict]) -> bytes: mapping_dict = { "id": "编号", "name": "岗位名称", @@ -185,11 +94,8 @@ class PositionService: "created_id": "创建者ID", "updated_id": "更新者ID", } - - # 复制数据并转换状态 data = position_list.copy() for item in data: item["status"] = "启用" if item.get("status") == 0 else "停用" item["creator"] = item.get("created_by", {}).get("name", "未知") if isinstance(item.get("created_by"), dict) else "未知" - return ExcelUtil.export_list2excel(list_data=data, mapping_dict=mapping_dict) diff --git a/backend/app/api/v1/module_system/role/controller.py b/backend/app/api/v1/module_system/role/controller.py index 948ceb37..5dc30bfa 100644 --- a/backend/app/api/v1/module_system/role/controller.py +++ b/backend/app/api/v1/module_system/role/controller.py @@ -21,11 +21,10 @@ from .schema import ( ) from .service import RoleService -RoleRouter = APIRouter(route_class=OperationLogRoute, prefix="/role", tags=["角色管理"]) +RoleRouter = APIRouter(route_class=OperationLogRoute, prefix="/role", tags=["系统管理", "角色管理"]) _ROLE_NS = "role" - @RoleRouter.get( "/list", summary="查询角色", @@ -37,22 +36,10 @@ async def get_role_list_controller( search: Annotated[RoleQueryParam, Depends()], auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:role:query"]))], ) -> JSONResponse: - """ - 查询角色 - - 参数: - - page (PaginationQueryParam): 分页查询参数模型 - - search (RoleQueryParam): 查询参数模型 - - auth (AuthSchema): 认证信息模型 - - 返回: - - JSONResponse: 分页查询结果JSON响应 - """ order_by = [{"order": "asc"}] if page.order_by: order_by = page.order_by - result_dict = await RoleService.page_service( - auth=auth, + result_dict = await RoleService(auth).page( page_no=page.page_no, page_size=page.page_size, search=search, @@ -60,7 +47,6 @@ async def get_role_list_controller( ) return SuccessResponse(data=result_dict, msg="查询角色成功") - @RoleRouter.get( "/detail/{id}", summary="查询角色详情", @@ -70,20 +56,9 @@ async def get_role_detail_controller( id: Annotated[int, Path(description="角色ID")], auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:role:detail"]))], ) -> JSONResponse: - """ - 查询角色详情 - - 参数: - - id (int): 角色ID - - auth (AuthSchema): 认证信息模型 - - 返回: - - JSONResponse: 角色详情JSON响应 - """ - result_dict = await RoleService.detail_service(id=id, auth=auth) + result_dict = await RoleService(auth).detail(id=id) return SuccessResponse(data=result_dict, msg="获取角色详情成功") - @RoleRouter.post( "/create", summary="创建角色", @@ -93,21 +68,10 @@ async def create_role_controller( data: RoleCreateSchema, auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:role:create"]))], ) -> JSONResponse: - """ - 创建角色 - - 参数: - - data (RoleCreateSchema): 创建角色模型 - - auth (AuthSchema): 认证信息模型 - - 返回: - - JSONResponse: 创建角色JSON响应 - """ - result_dict = await RoleService.create_service(data=data, auth=auth) + result_dict = await RoleService(auth).create(data=data) await FastAPICache.clear(namespace=_ROLE_NS) return SuccessResponse(data=result_dict, msg="创建角色成功") - @RoleRouter.put( "/update/{id}", summary="修改角色", @@ -118,22 +82,10 @@ async def update_role_controller( id: Annotated[int, Path(description="角色ID")], auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:role:update"]))], ) -> JSONResponse: - """ - 修改角色 - - 参数: - - data (RoleUpdateSchema): 修改角色模型 - - id (int): 角色ID - - auth (AuthSchema): 认证信息模型 - - 返回: - - JSONResponse: 修改角色JSON响应 - """ - result_dict = await RoleService.update_service(id=id, data=data, auth=auth) + result_dict = await RoleService(auth).update(id=id, data=data) await FastAPICache.clear(namespace=_ROLE_NS) return SuccessResponse(data=result_dict, msg="修改角色成功") - @RoleRouter.delete( "/delete", summary="删除角色", @@ -143,21 +95,10 @@ async def delete_role_controller( ids: Annotated[list[int], Body(description="ID列表")], auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:role:delete"]))], ) -> JSONResponse: - """ - 删除角色 - - 参数: - - ids (list[int]): ID列表 - - auth (AuthSchema): 认证信息模型 - - 返回: - - JSONResponse: 删除角色JSON响应 - """ - await RoleService.delete_service(ids=ids, auth=auth) + await RoleService(auth).delete(ids=ids) await FastAPICache.clear(namespace=_ROLE_NS) return SuccessResponse(msg="删除角色成功") - @RoleRouter.patch( "/status/batch", summary="批量修改角色状态", @@ -167,21 +108,10 @@ async def batch_set_available_role_controller( data: BatchSetAvailable, auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:role:patch"]))], ) -> JSONResponse: - """ - 批量修改角色状态 - - 参数: - - data (BatchSetAvailable): 批量修改角色状态模型 - - auth (AuthSchema): 认证信息模型 - - 返回: - - JSONResponse: 批量修改角色状态JSON响应 - """ - await RoleService.set_available_service(data=data, auth=auth) + await RoleService(auth).set_available(data=data) await FastAPICache.clear(namespace=_ROLE_NS) return SuccessResponse(msg="批量修改角色状态成功") - @RoleRouter.put( "/permission", summary="角色授权", @@ -191,21 +121,10 @@ async def set_role_permission_controller( data: RolePermissionSettingSchema, auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:role:permission"]))], ) -> JSONResponse: - """ - 角色授权 - - 参数: - - data (RolePermissionSettingSchema): 角色授权模型 - - auth (AuthSchema): 认证信息模型 - - 返回: - - JSONResponse: 角色授权JSON响应 - """ - await RoleService.set_permission_service(data=data, auth=auth) + await RoleService(auth).set_permission(data=data) await FastAPICache.clear(namespace=_ROLE_NS) return SuccessResponse(msg="授权角色成功") - @RoleRouter.get( "/export", summary="导出角色", @@ -215,18 +134,8 @@ async def export_role_list_controller( search: Annotated[RoleQueryParam, Depends()], auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:role:export"]))], ) -> StreamingResponse: - """ - 导出角色 - - 参数: - - search (RoleQueryParam): 查询参数模型 - - auth (AuthSchema): 认证信息模型 - - 返回: - - StreamingResponse: 导出角色流响应 - """ - role_query_result = await RoleService.list_service(search=search, auth=auth) - role_export_result = await RoleService.export_list_service(role_list=role_query_result) + role_query_result = await RoleService(auth).list(search=search) + role_export_result = RoleService.export_list(role_list=role_query_result) return StreamResponse( data=bytes2file_response(role_export_result), diff --git a/backend/app/api/v1/module_system/role/model.py b/backend/app/api/v1/module_system/role/model.py index 039081a3..39fad178 100644 --- a/backend/app/api/v1/module_system/role/model.py +++ b/backend/app/api/v1/module_system/role/model.py @@ -78,7 +78,7 @@ class RoleModel(ModelMixin, TenantMixin, UserMixin): "deleted_by", "tenant_by", ] - __permission_strategy__: PermissionFilterStrategy = PermissionFilterStrategy.USER_ROLE + __permission_strategy__: PermissionFilterStrategy = PermissionFilterStrategy.USER_BINDING name: Mapped[str] = mapped_column(String(64), nullable=False, comment="角色名称") code: Mapped[str] = mapped_column(String(64), nullable=False, comment="角色编码") diff --git a/backend/app/api/v1/module_system/role/service.py b/backend/app/api/v1/module_system/role/service.py index 8c9829e6..34f25db9 100644 --- a/backend/app/api/v1/module_system/role/service.py +++ b/backend/app/api/v1/module_system/role/service.py @@ -22,24 +22,23 @@ class RoleService: 提供角色 CRUD、权限配置、数据权限范围设置、批量启/禁用、Excel 导出等业务能力。 """ - @classmethod - async def detail_service(cls, auth: AuthSchema, id: int) -> RoleOutSchema: + def __init__(self, auth: AuthSchema) -> None: + self.auth = auth + + async def detail(self, id: int) -> RoleOutSchema: """ 获取角色详情 参数: - - auth (AuthSchema): 认证信息模型 - id (int): 角色ID 返回: - RoleOutSchema: 角色详情响应模型 """ - return await RoleCRUD(auth).get_or_404(id=id, out_schema=RoleOutSchema) + return await RoleCRUD(self.auth).get_or_404(id=id, out_schema=RoleOutSchema) - @classmethod - async def list_service( - cls, - auth: AuthSchema, + async def list( + self, search: RoleQueryParam | None = None, order_by: list[dict[str, str]] | None = None, ) -> list[RoleOutSchema]: @@ -47,20 +46,17 @@ class RoleService: 获取角色列表 参数: - - auth (AuthSchema): 认证信息模型 - search (RoleQueryParam | None): 查询参数模型 - order_by (list[dict[str, str]] | None): 排序参数列表 返回: - list[RoleOutSchema]: 角色响应模型列表 """ - role_list = await RoleCRUD(auth).list(search=vars(search) if search else None, order_by=order_by) + role_list = await RoleCRUD(self.auth).list(search=vars(search) if search else None, order_by=order_by) return [RoleOutSchema.model_validate(role) for role in role_list] - @classmethod - async def page_service( - cls, - auth: AuthSchema, + async def page( + self, page_no: int, page_size: int, search: RoleQueryParam | None = None, @@ -70,17 +66,16 @@ class RoleService: 分页查询角色(数据库 OFFSET/LIMIT)。 参数: - - auth (AuthSchema): 认证信息模型 - page_no (int): 页码(从 1 开始) - page_size (int): 每页条数 - search (RoleQueryParam | None): 查询条件 - order_by (list[dict[str, str]] | None): 排序字段列表 返回: - - dict: 分页结果(结构由 `CRUD.page` 返回约定) + - dict: 分页结果(结构由 ``CRUD.page`` 返回约定) """ offset = (page_no - 1) * page_size - return await RoleCRUD(auth).page( + return await RoleCRUD(self.auth).page( offset=offset, limit=page_size, order_by=order_by or [{"id": "asc"}], @@ -88,61 +83,55 @@ class RoleService: out_schema=RoleOutSchema, ) - @classmethod - async def create_service(cls, auth: AuthSchema, data: RoleCreateSchema) -> RoleOutSchema: + async def create(self, data: RoleCreateSchema) -> RoleOutSchema: """ 创建角色 参数: - - auth (AuthSchema): 认证信息模型 - data (RoleCreateSchema): 创建角色模型 返回: - RoleOutSchema: 新创建的角色响应模型 """ - role = await RoleCRUD(auth).get(name=data.name) + role = await RoleCRUD(self.auth).get(name=data.name) if role: raise CustomException(msg="创建失败,该数据已存在") - obj = await RoleCRUD(auth).get(code=data.code) + obj = await RoleCRUD(self.auth).get(code=data.code) if obj: raise CustomException(msg="创建失败,编码已存在") # 检查租户配额 - await TenantService.check_quota_service(auth, auth.tenant_id, "role") + await TenantService(self.auth).check_quota(self.auth.tenant_id, "role") - new_role = await RoleCRUD(auth).create(data=data) + new_role = await RoleCRUD(self.auth).create(data=data) return RoleOutSchema.model_validate(new_role) - @classmethod - async def update_service(cls, auth: AuthSchema, id: int, data: RoleUpdateSchema) -> RoleOutSchema: + async def update(self, id: int, data: RoleUpdateSchema) -> RoleOutSchema: """ 更新角色 参数: - - auth (AuthSchema): 认证信息模型 - id (int): 角色ID - data (RoleUpdateSchema): 更新角色模型 返回: - RoleOutSchema: 更新后的角色响应模型 """ - _ = await RoleCRUD(auth).get_or_404(id=id, msg="更新失败,该数据不存在") - exist_role = await RoleCRUD(auth).get(name=data.name) + _ = await RoleCRUD(self.auth).get_or_404(id=id, msg="更新失败,该数据不存在") + exist_role = await RoleCRUD(self.auth).get(name=data.name) if exist_role and exist_role.id != id: raise CustomException(msg="更新失败,名称已存在") - exist_code = await RoleCRUD(auth).get(code=data.code) + exist_code = await RoleCRUD(self.auth).get(code=data.code) if exist_code and exist_code.id != id: raise CustomException(msg="更新失败,角色编码已存在") - updated_role = await RoleCRUD(auth).update(id=id, data=data) + updated_role = await RoleCRUD(self.auth).update(id=id, data=data) return RoleOutSchema.model_validate(updated_role) - @classmethod - async def delete_service(cls, auth: AuthSchema, ids: list[int]) -> None: + async def delete(self, ids: list[int]) -> None: """ 删除角色 参数: - - auth (AuthSchema): 认证信息模型 - ids (list[int]): 角色ID列表 返回: @@ -152,57 +141,53 @@ class RoleService: raise CustomException(msg="删除失败,删除对象不能为空") # 批量校验角色存在性 - roles = await RoleCRUD(auth).list(search={"id": ("in", ids)}) + roles = await RoleCRUD(self.auth).list(search={"id": ("in", ids)}) if len(roles) != len(ids): raise CustomException(msg="删除失败,部分ID不存在") - await RoleCRUD(auth).delete(ids=ids) + await RoleCRUD(self.auth).delete(ids=ids) - @classmethod - async def set_permission_service(cls, auth: AuthSchema, data: RolePermissionSettingSchema) -> None: + async def set_permission(self, data: RolePermissionSettingSchema) -> None: """ 设置角色权限 参数: - - auth (AuthSchema): 认证信息模型 - data (RolePermissionSettingSchema): 角色权限设置模型 返回: - None """ # 设置角色菜单权限 - await RoleCRUD(auth).set_role_menus_crud(role_ids=data.role_ids, menu_ids=data.menu_ids) + await RoleCRUD(self.auth).set_role_menus_crud(role_ids=data.role_ids, menu_ids=data.menu_ids) # 设置数据权限范围 - await RoleCRUD(auth).set(ids=data.role_ids, data_scope=data.data_scope) + await RoleCRUD(self.auth).set(ids=data.role_ids, data_scope=data.data_scope) # 设置自定义数据权限部门 if data.data_scope == 5 and data.dept_ids: - await RoleCRUD(auth).set_role_depts_crud(role_ids=data.role_ids, dept_ids=data.dept_ids) + await RoleCRUD(self.auth).set_role_depts_crud(role_ids=data.role_ids, dept_ids=data.dept_ids) else: - await RoleCRUD(auth).set_role_depts_crud(role_ids=data.role_ids, dept_ids=[]) + await RoleCRUD(self.auth).set_role_depts_crud(role_ids=data.role_ids, dept_ids=[]) - @classmethod - async def set_available_service(cls, auth: AuthSchema, data: BatchSetAvailable) -> None: + async def set_available(self, data: BatchSetAvailable) -> None: """ 设置角色可用状态 参数: - - auth (AuthSchema): 认证信息模型 - data (BatchSetAvailable): 批量设置可用状态模型 返回: - None """ - roles = await RoleCRUD(auth).list(search={"id": ("in", data.ids)}) + roles = await RoleCRUD(self.auth).list(search={"id": ("in", data.ids)}) role_map = {r.id: r for r in roles} for rid in data.ids: if rid not in role_map: raise CustomException(msg="该数据不存在") - await RoleCRUD(auth).set(ids=data.ids, status=data.status) + await RoleCRUD(self.auth).set(ids=data.ids, status=data.status) - @classmethod - async def export_list_service(cls, role_list: list[dict[str, Any]]) -> bytes: + @staticmethod + def export_list(role_list: list[dict[str, Any]]) -> bytes: """ 导出角色列表 diff --git a/backend/app/api/v1/module_system/ticket/controller.py b/backend/app/api/v1/module_system/ticket/controller.py index a029300e..da1f734b 100644 --- a/backend/app/api/v1/module_system/ticket/controller.py +++ b/backend/app/api/v1/module_system/ticket/controller.py @@ -1,6 +1,6 @@ from typing import Annotated -from fastapi import APIRouter, Body, Depends, Path +from fastapi import APIRouter, Depends from fastapi.responses import JSONResponse from app.common.response import ResponseSchema, SuccessResponse @@ -18,28 +18,15 @@ from .schema import ( ) from .service import TicketService -TicketRouter = APIRouter(route_class=OperationLogRoute, prefix="/ticket", tags=["工单管理"]) - +TicketRouter = APIRouter(route_class=OperationLogRoute, prefix="/ticket", tags=["系统管理", "工单管理"]) @TicketRouter.get("/list", summary="工单列表", response_model=ResponseSchema[PageResultSchema[TicketOutSchema]]) async def ticket_list_controller( - page: Annotated[PaginationQueryParam, Depends()], search: Annotated[TicketQueryParam, Depends()], - auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:ticket:query"]))], + page: Annotated[PaginationQueryParam, Depends()], + auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:ticket:list"]))], ) -> JSONResponse: - """ - 工单列表 - - 参数: - - page (PaginationQueryParam): 分页查询参数。 - - search (TicketQueryParam): 查询筛选参数。 - - auth (AuthSchema): 认证信息模型。 - - 返回: - - JSONResponse: 包含分页工单列表的 JSON 响应。 - """ - result = await TicketService.page_service( - auth=auth, + result = await TicketService(auth).page( page_no=page.page_no, page_size=page.page_size, search=search, @@ -47,99 +34,43 @@ async def ticket_list_controller( ) return SuccessResponse(data=result, msg="查询成功") - -@TicketRouter.get("/detail/{id}", summary="工单详情", response_model=ResponseSchema[TicketOutSchema]) +@TicketRouter.get("/detail/{id}", summary="获取工单详情", response_model=ResponseSchema[TicketOutSchema]) async def ticket_detail_controller( - id: Annotated[int, Path(description="ID")], - auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:ticket:query"]))], + id: int, + auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:ticket:detail"]))], ) -> JSONResponse: - """ - 工单详情 - - 参数: - - id (int): 工单 ID。 - - auth (AuthSchema): 认证信息模型。 - - 返回: - - JSONResponse: 包含工单详情的 JSON 响应。 - """ - result = await TicketService.detail_service(auth=auth, id=id) + result = await TicketService(auth).detail(id=id) return SuccessResponse(data=result, msg="查询成功") - @TicketRouter.post("/create", summary="创建工单", response_model=ResponseSchema[TicketOutSchema]) async def ticket_create_controller( data: TicketCreateSchema, auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:ticket:create"]))], ) -> JSONResponse: - """ - 创建工单 - - 参数: - - data (TicketCreateSchema): 工单创建参数。 - - auth (AuthSchema): 认证信息模型。 - - 返回: - - JSONResponse: 包含创建后的工单详情的 JSON 响应。 - """ - result = await TicketService.create_service(auth=auth, data=data) + result = await TicketService(auth).create(data=data) return SuccessResponse(data=result, msg="创建成功") - @TicketRouter.put("/update/{id}", summary="更新工单", response_model=ResponseSchema[TicketOutSchema]) async def ticket_update_controller( - id: Annotated[int, Path(description="ID")], + id: int, data: TicketUpdateSchema, auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:ticket:update"]))], ) -> JSONResponse: - """ - 更新工单 - - 参数: - - id (int): 工单 ID。 - - data (TicketUpdateSchema): 工单更新参数。 - - auth (AuthSchema): 认证信息模型。 - - 返回: - - JSONResponse: 包含更新后的工单详情的 JSON 响应。 - """ - result = await TicketService.update_service(auth=auth, id=id, data=data) + result = await TicketService(auth).update(id=id, data=data) return SuccessResponse(data=result, msg="更新成功") - @TicketRouter.put("/batch", summary="批量更新工单", response_model=ResponseSchema) async def ticket_batch_update_controller( data: TicketBatchSchema, auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:ticket:update"]))], ) -> JSONResponse: - """ - 批量更新工单 - - 参数: - - data (TicketBatchSchema): 批量更新参数(ID 列表 + 状态)。 - - auth (AuthSchema): 认证信息模型。 - - 返回: - - JSONResponse: 操作结果。 - """ - await TicketService.batch_service(auth=auth, data=data) - return SuccessResponse(msg="批量更新成功") - + await TicketService(auth).batch(data=data) + return SuccessResponse(msg="批量操作成功") @TicketRouter.delete("/delete", summary="删除工单", response_model=ResponseSchema[None]) async def ticket_delete_controller( - ids: Annotated[list[int], Body()], + ids: list[int], auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:ticket:delete"]))], ) -> JSONResponse: - """ - 删除工单 - - 参数: - - ids (list[int]): 工单 ID 列表。 - - auth (AuthSchema): 认证信息模型。 - - 返回: - - JSONResponse: 删除结果。 - """ - await TicketService.delete_service(auth=auth, ids=ids) + await TicketService(auth).delete(ids=ids) return SuccessResponse(msg="删除成功") diff --git a/backend/app/api/v1/module_system/ticket/service.py b/backend/app/api/v1/module_system/ticket/service.py index 9cad23c2..458c9f8b 100644 --- a/backend/app/api/v1/module_system/ticket/service.py +++ b/backend/app/api/v1/module_system/ticket/service.py @@ -29,30 +29,12 @@ _TICKET_STATUS_LABELS = { class TicketService: - """ - 工单管理服务 + """工单管理服务""" - 提供工单 CRUD、状态流转校验、批量更新、分配处理人等业务能力。 - """ + def __init__(self, auth: AuthSchema) -> None: + self.auth = auth - @classmethod - def _validate_status_transition( - cls, - auth: AuthSchema, - ticket, - new_status: int, - ) -> None: - """ - 校验工单状态流转是否合法 - - 参数: - - auth (AuthSchema): 认证信息模型 - - ticket: 工单对象 - - new_status (int): 新状态 - - 异常: - - CustomException: 状态流转不合法或权限不足 - """ + def _validate_status_transition(self, ticket, new_status: int) -> None: old_status = ticket.status if ticket.status is not None else 0 old_label = _TICKET_STATUS_LABELS.get(old_status, str(old_status)) new_label = _TICKET_STATUS_LABELS.get(new_status, str(new_status)) @@ -60,9 +42,9 @@ class TicketService: if new_status not in _TICKET_STATUS_TRANSITIONS.get(old_status, set()): raise CustomException(msg=f"不允许从{old_label}转换为{new_label}") - is_super = auth.user and auth.user.is_superuser - is_creator = auth.user and ticket.created_id == auth.user.id - is_assignee = auth.user and ticket.assigned_id == auth.user.id + is_super = self.auth.user and self.auth.user.is_superuser + is_creator = self.auth.user and ticket.created_id == self.auth.user.id + is_assignee = self.auth.user and ticket.assigned_id == self.auth.user.id if new_status == 0: if not is_super: @@ -83,29 +65,14 @@ class TicketService: if not (is_super or is_creator): raise CustomException(msg="仅创建人或超管可以确认关闭工单") - @classmethod - async def page_service( - cls, - auth: AuthSchema, + async def page( + self, page_no: int, page_size: int, search: TicketQueryParam | None = None, order_by: list | None = None, ) -> dict: - """ - 分页查询工单 - - 参数: - - auth (AuthSchema): 认证信息模型 - - page_no (int): 页码 - - page_size (int): 每页条数 - - search (TicketQueryParam | None): 查询参数 - - order_by (list | None): 排序参数 - - 返回: - - dict: 分页结果 - """ - return await TicketCRUD(auth).page( + return await TicketCRUD(self.auth).page( offset=(page_no - 1) * page_size, limit=page_size, order_by=order_by or [{"created_time": "desc"}], @@ -113,103 +80,52 @@ class TicketService: out_schema=TicketOutSchema, ) - @classmethod - async def detail_service(cls, auth: AuthSchema, id: int) -> TicketOutSchema: - """ - 获取工单详情 + async def detail(self, id: int) -> TicketOutSchema: + return await TicketCRUD(self.auth).get_or_404(id=id, out_schema=TicketOutSchema) - 参数: - - auth (AuthSchema): 认证信息模型 - - id (int): 工单ID - - 返回: - - TicketOutSchema: 工单详情响应模型 - """ - return await TicketCRUD(auth).get_or_404(id=id, out_schema=TicketOutSchema) - - @classmethod - async def create_service(cls, auth: AuthSchema, data: TicketCreateSchema) -> TicketOutSchema: - """ - 创建工单 - - 参数: - - auth (AuthSchema): 认证信息模型 - - data (TicketCreateSchema): 工单创建数据 - - 返回: - - TicketOutSchema: 创建后的工单响应模型 - """ - obj = await TicketCRUD(auth).create(data=data) + async def create(self, data: TicketCreateSchema) -> TicketOutSchema: + obj = await TicketCRUD(self.auth).create(data=data) if not obj: raise CustomException(msg="创建工单失败") return TicketOutSchema.model_validate(obj) - @classmethod - async def update_service(cls, auth: AuthSchema, id: int, data: TicketUpdateSchema) -> TicketOutSchema: - """ - 更新工单 - - 参数: - - auth (AuthSchema): 认证信息模型 - - id (int): 工单ID - - data (TicketUpdateSchema): 工单更新数据 - - 返回: - - TicketOutSchema: 更新后的工单响应模型 - """ - obj = await TicketCRUD(auth).get_or_404(id=id, msg="工单不存在") + async def update(self, id: int, data: TicketUpdateSchema) -> TicketOutSchema: + obj = await TicketCRUD(self.auth).get_or_404(id=id, msg="工单不存在") if data.status is not None: - cls._validate_status_transition(auth, obj, data.status) + self._validate_status_transition(obj, data.status) - # 校验 assigned_id:分配处理人时验证用户是否存在且属于同一租户 if data.assigned_id is not None: user_stmt = select(UserModel).where( UserModel.id == data.assigned_id, UserModel.is_deleted.is_(False), ) - user_result = await auth.db.execute(user_stmt) + user_result = await self.auth.db.execute(user_stmt) assigned_user = user_result.scalar_one_or_none() if not assigned_user: raise CustomException(msg="指定的处理人不存在") if assigned_user.tenant_id != obj.tenant_id: raise CustomException(msg="处理人必须与工单属于同一租户") - updated = await TicketCRUD(auth).update(id=id, data=data) + updated = await TicketCRUD(self.auth).update(id=id, data=data) if not updated: raise CustomException(msg="工单不存在") return TicketOutSchema.model_validate(updated) - @classmethod - async def delete_service(cls, auth: AuthSchema, ids: list[int]) -> None: - """ - 删除工单 - - 参数: - - auth (AuthSchema): 认证信息模型 - - ids (list[int]): 工单ID列表 - """ + async def delete(self, ids: list[int]) -> None: if not ids: raise CustomException(msg="删除对象不能为空") - await TicketCRUD(auth).delete(ids=ids) + await TicketCRUD(self.auth).delete(ids=ids) - @classmethod - async def batch_service(cls, auth: AuthSchema, data: TicketBatchSchema) -> None: - """ - 批量更新工单状态 - - 参数: - - auth (AuthSchema): 认证信息模型 - - data (TicketBatchSchema): 批量更新参数 - """ + async def batch(self, data: TicketBatchSchema) -> None: if not data.ids: raise CustomException(msg="请选择要操作的工单") - tickets = await TicketCRUD(auth).get_by_ids_crud(ids=data.ids) + tickets = await TicketCRUD(self.auth).get_by_ids_crud(ids=data.ids) ticket_map = {t.id: t for t in tickets} for tid in data.ids: obj = ticket_map.get(tid) if not obj: raise CustomException(msg=f"工单[{tid}]不存在") - cls._validate_status_transition(auth, obj, data.status) - await TicketCRUD(auth).set_crud(ids=data.ids, status=data.status) + self._validate_status_transition(obj, data.status) + await TicketCRUD(self.auth).set_crud(ids=data.ids, status=data.status) diff --git a/backend/app/api/v1/module_system/user/controller.py b/backend/app/api/v1/module_system/user/controller.py index 74ad3307..20e3f129 100644 --- a/backend/app/api/v1/module_system/user/controller.py +++ b/backend/app/api/v1/module_system/user/controller.py @@ -26,8 +26,7 @@ from .schema import ( ) from .service import UserService -UserRouter = APIRouter(route_class=OperationLogRoute, prefix="/user", tags=["用户管理"]) - +UserRouter = APIRouter(route_class=OperationLogRoute, prefix="/user", tags=["系统管理", "用户管理"]) @UserRouter.get( "/current/info", @@ -37,19 +36,9 @@ UserRouter = APIRouter(route_class=OperationLogRoute, prefix="/user", tags=["用 async def get_current_user_info_controller( auth: Annotated[AuthSchema, Depends(get_current_user)], ) -> JSONResponse: - """ - 查询当前用户信息 - - 参数: - - auth (AuthSchema): 认证信息模型 - - 返回: - - JSONResponse: 当前用户信息JSON响应 - """ - user_dict = await UserService.current_info_service(auth=auth) + user_dict = await UserService(auth).current_info() return SuccessResponse(data=user_dict, msg="获取当前用户信息成功") - @UserRouter.put( "/current/info/update", summary="更新当前用户基本信息", @@ -59,20 +48,9 @@ async def update_current_user_info_controller( data: CurrentUserUpdateSchema, auth: Annotated[AuthSchema, Depends(get_current_user)], ) -> JSONResponse: - """ - 更新当前用户基本信息 - - 参数: - - data (CurrentUserUpdateSchema): 当前用户更新模型 - - auth (AuthSchema): 认证信息模型 - - 返回: - - JSONResponse: 更新当前用户基本信息JSON响应 - """ - result_dict = await UserService.update_current_info_service(auth=auth, data=data) + result_dict = await UserService(auth).update_current_info(data=data) return SuccessResponse(data=result_dict, msg="更新当前用户基本信息成功") - @UserRouter.put( "/password/change", summary="修改当前用户密码", @@ -82,20 +60,9 @@ async def change_current_user_password_controller( data: UserChangePasswordSchema, auth: Annotated[AuthSchema, Depends(get_current_user)], ) -> JSONResponse: - """ - 修改当前用户密码 - - 参数: - - data (UserChangePasswordSchema): 用户密码修改模型 - - auth (AuthSchema): 认证信息模型 - - 返回: - - JSONResponse: 修改密码JSON响应 - """ - result_dict = await UserService.change_password_service(auth=auth, data=data) + result_dict = await UserService(auth).change_password(data=data) return SuccessResponse(data=result_dict, msg="修改密码成功, 请重新登录") - @UserRouter.put( "/password/reset/{id}", summary="重置用户密码", @@ -106,22 +73,10 @@ async def reset_password_controller( data: ResetPasswordSchema, auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:user:update"]))], ) -> JSONResponse: - """ - 重置密码 - - 参数: - - id (int): 用户ID - - data (ResetPasswordSchema): 重置密码模型 - - auth (AuthSchema): 认证信息模型 - - 返回: - - JSONResponse: 重置密码JSON响应 - """ data.id = id - result_dict = await UserService.reset_password_service(auth=auth, data=data) + result_dict = await UserService(auth).reset_password(data=data) return SuccessResponse(data=result_dict, msg="重置密码成功") - @UserRouter.post( "/register", summary="注册用户", @@ -131,22 +86,11 @@ async def register_user_controller( data: UserRegisterSchema, db: Annotated[AsyncSession, Depends(db_getter)], ) -> JSONResponse: - """ - 注册用户 - - 参数: - - data (UserRegisterSchema): 用户注册模型 - - db (AsyncSession): 异步数据库会话 - - 返回: - - JSONResponse: 注册用户JSON响应 - """ - auth = AuthSchema(db=db) - user_register_result = await UserService.register_service(data=data, auth=auth) + auth = AuthSchema(db=db, check_data_scope=False) + user_register_result = await UserService(auth).register(data=data) logger.info(f"{data.username} 注册用户成功: {user_register_result}") return SuccessResponse(data=user_register_result, msg="注册用户成功") - @UserRouter.post( "/password/forget", summary="忘记密码", @@ -156,22 +100,11 @@ async def forget_password_controller( data: UserForgetPasswordSchema, db: Annotated[AsyncSession, Depends(db_getter)], ) -> JSONResponse: - """ - 忘记密码 - - 参数: - - data (UserForgetPasswordSchema): 用户忘记密码模型 - - db (AsyncSession): 异步数据库会话 - - 返回: - - JSONResponse: 忘记密码JSON响应 - """ - auth = AuthSchema(db=db) - user_forget_password_result = await UserService.forget_password_service(data=data, auth=auth) + auth = AuthSchema(db=db, check_data_scope=False) + user_forget_password_result = await UserService(auth).forget_password(data=data) logger.info(f"{data.username} 重置密码成功: {user_forget_password_result}") return SuccessResponse(data=user_forget_password_result, msg="重置密码成功") - @UserRouter.get( "/list", summary="查询用户", @@ -182,19 +115,7 @@ async def get_user_list_controller( search: Annotated[UserQueryParam, Depends()], auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:user:query"]))], ) -> JSONResponse: - """ - 查询用户 - - 参数: - - page (PaginationQueryParam): 分页查询参数模型 - - search (UserQueryParam): 查询参数模型 - - auth (AuthSchema): 认证信息模型 - - 返回: - - JSONResponse: 分页查询结果JSON响应 - """ - result_dict = await UserService.page_service( - auth=auth, + result_dict = await UserService(auth).page( page_no=page.page_no, page_size=page.page_size, search=search, @@ -202,7 +123,6 @@ async def get_user_list_controller( ) return SuccessResponse(data=result_dict, msg="查询用户成功") - @UserRouter.get( "/detail/{id}", summary="查询用户详情", @@ -212,20 +132,9 @@ async def get_user_detail_controller( id: Annotated[int, Path(description="用户ID")], auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:user:detail"]))], ) -> JSONResponse: - """ - 查询用户详情 - - 参数: - - id (int): 用户ID - - auth (AuthSchema): 认证信息模型 - - 返回: - - JSONResponse: 用户详情JSON响应 - """ - result_dict = await UserService.detail_service(auth=auth, id=id) + result_dict = await UserService(auth).detail(id=id) return SuccessResponse(data=result_dict, msg="获取用户详情成功") - @UserRouter.post( "/create", summary="创建用户", @@ -235,24 +144,9 @@ async def create_user_controller( data: UserCreateSchema, auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:user:create"]))], ) -> JSONResponse: - """ - 创建用户 - - **注意**: - - 创建用户时, 默认密码为: - - 创建用户时, 默认用户状态为: 启用 - - 参数: - - data (UserCreateSchema): 用户创建模型 - - auth (AuthSchema): 认证信息模型 - - 返回: - - JSONResponse: 创建用户JSON响应 - """ - result_dict = await UserService.create_service(data=data, auth=auth) + result_dict = await UserService(auth).create(data=data) return SuccessResponse(data=result_dict, msg="创建用户成功") - @UserRouter.put( "/update/{id}", summary="修改用户", @@ -263,21 +157,9 @@ async def update_user_controller( id: Annotated[int, Path(description="用户ID")], auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:user:update"]))], ) -> JSONResponse: - """ - 修改用户 - - 参数: - - data (UserUpdateSchema): 用户修改模型 - - id (int): 用户ID - - auth (AuthSchema): 认证信息模型 - - 返回: - - JSONResponse: 修改用户JSON响应 - """ - result_dict = await UserService.update_service(auth=auth, id=id, data=data) + result_dict = await UserService(auth).update(id=id, data=data) return SuccessResponse(data=result_dict, msg="修改用户成功") - @UserRouter.delete( "/delete", summary="删除用户", @@ -287,20 +169,9 @@ async def delete_user_controller( ids: Annotated[list[int], Body(description="ID列表")], auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:user:delete"]))], ) -> JSONResponse: - """ - 删除用户 - - 参数: - - ids (list[int]): 用户ID列表 - - auth (AuthSchema): 认证信息模型 - - 返回: - - JSONResponse: 删除用户JSON响应 - """ - await UserService.delete_service(auth=auth, ids=ids) + await UserService(auth).delete(ids=ids) return SuccessResponse(msg="删除用户成功") - @UserRouter.patch( "/status/batch", summary="批量修改用户状态", @@ -310,20 +181,9 @@ async def batch_set_available_user_controller( data: BatchSetAvailable, auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:user:patch"]))], ) -> JSONResponse: - """ - 批量修改用户状态 - - 参数: - - data (BatchSetAvailable): 批量修改用户状态模型 - - auth (AuthSchema): 认证信息模型 - - 返回: - - JSONResponse: 批量修改用户状态JSON响应 - """ - await UserService.set_available_service(auth=auth, data=data) + await UserService(auth).set_available(data=data) return SuccessResponse(msg="批量修改用户状态成功") - @UserRouter.get( "/import/template", summary="获取用户导入模板", @@ -331,13 +191,7 @@ async def batch_set_available_user_controller( dependencies=[Depends(AuthPermission(["module_system:user:download"]))], ) async def export_user_import_template_controller() -> StreamingResponse: - """ - 获取用户导入模板 - - 返回: - - StreamingResponse: 用户导入模板流响应 - """ - user_import_template_result = await UserService.get_import_template_service() + user_import_template_result = UserService.get_import_template() return StreamResponse( data=bytes2file_response(user_import_template_result), @@ -348,7 +202,6 @@ async def export_user_import_template_controller() -> StreamingResponse: }, ) - @UserRouter.get( "/export", summary="导出用户", @@ -359,19 +212,8 @@ async def export_user_list_controller( search: Annotated[UserQueryParam, Depends()], auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:user:export"]))], ) -> StreamingResponse: - """ - 导出用户 - - 参数: - - page (PaginationQueryParam): 分页查询参数模型 - - search (UserQueryParam): 查询参数模型 - - auth (AuthSchema): 认证信息模型 - - 返回: - - StreamingResponse: 用户导出模板流响应 - """ - user_list = await UserService.list_service(auth=auth, search=search, order_by=page.order_by) - user_export_result = await UserService.export_list_service(user_list=user_list) + user_list = await UserService(auth).list(search=search, order_by=page.order_by) + user_export_result = UserService.export_list(user_list=user_list) return StreamResponse( data=bytes2file_response(user_export_result), @@ -379,7 +221,6 @@ async def export_user_list_controller( headers={"Content-Disposition": "attachment; filename=user.xlsx"}, ) - @UserRouter.post( "/import/data", summary="导入用户", @@ -389,15 +230,5 @@ async def import_user_list_controller( file: UploadFile, auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:user:import"]))], ) -> JSONResponse: - """ - 导入用户 - - 参数: - - file (UploadFile): 用户导入文件 - - auth (AuthSchema): 认证信息模型 - - 返回: - - JSONResponse: 导入用户JSON响应 - """ - batch_import_result = await UserService.batch_import_service(auth=auth, file=file, update_support=True) + batch_import_result = await UserService(auth).batch_import(file=file, update_support=True) return SuccessResponse(data=batch_import_result, msg="导入用户成功") diff --git a/backend/app/api/v1/module_system/user/service.py b/backend/app/api/v1/module_system/user/service.py index 72c8ec8c..404407b5 100644 --- a/backend/app/api/v1/module_system/user/service.py +++ b/backend/app/api/v1/module_system/user/service.py @@ -33,72 +33,36 @@ from .schema import ( class UserService: - """ - 用户管理服务 + """用户管理服务""" - 提供用户 CRUD、密码管理、状态切换、批量导入/导出、当前用户信息获取/更新、忘记密码/注册等业务能力。 - """ - - @classmethod - async def detail_service(cls, auth: AuthSchema, id: int) -> UserOutSchema: - """ - 根据ID获取用户详情 - - 参数: - - auth (AuthSchema): 认证信息模型 - - id (int): 用户ID - - 返回: - - dict: 用户详情字典 - """ - user = await UserCRUD(auth).get_or_404(id=id) + def __init__(self, auth: AuthSchema) -> None: + self.auth = auth + async def detail(self, id: int) -> UserOutSchema: + user = await UserCRUD(self.auth).get_or_404(id=id) result = UserOutSchema.model_validate(user) - # 如果用户绑定了部门,则获取部门名称 if user.dept_id: - dept = await DeptCRUD(auth).get(id=user.dept_id) + dept = await DeptCRUD(self.auth).get(id=user.dept_id) result.dept_name = dept.name if dept else None - return result - @classmethod - async def list_service(cls, auth: AuthSchema, search: UserQueryParam | None = None, order_by: list[dict[str, str]] | None = None) -> list[UserOutSchema]: - """ - 获取用户列表 + async def list( + self, + search: UserQueryParam | None = None, + order_by: list[dict[str, str]] | None = None, + ) -> list[UserOutSchema]: + user_list = await UserCRUD(self.auth).list(search=vars(search) if search else None, order_by=order_by) + return [UserOutSchema.model_validate(user) for user in user_list] - 参数: - - auth (AuthSchema): 认证信息模型 - - search (UserQueryParam | None): 查询参数对象。 - - order_by (list[dict[str, str]] | None): 排序参数列表。 - - 返回: - - list[dict]: 用户详情字典列表 - """ - user_list = await UserCRUD(auth).list(search=vars(search) if search else None, order_by=order_by) - user_dict_list = [] - for user in user_list: - user_dict = UserOutSchema.model_validate(user) - user_dict_list.append(user_dict) - - return user_dict_list - - @classmethod - async def page_service(cls, auth: AuthSchema, page_no: int, page_size: int, search: UserQueryParam | None = None, order_by: list[dict[str, str]] | None = None) -> dict: - """ - 分页查询用户(数据库 OFFSET/LIMIT)。 - - 参数: - - auth (AuthSchema): 认证信息模型 - - page_no (int): 页码(从 1 开始) - - page_size (int): 每页条数 - - search (UserQueryParam | None): 查询条件 - - order_by (list[dict[str, str]] | None): 排序字段列表 - - 返回: - - dict: 分页结果(结构由 `CRUD.page` 返回约定) - """ + async def page( + self, + page_no: int, + page_size: int, + search: UserQueryParam | None = None, + order_by: list[dict[str, str]] | None = None, + ) -> dict: offset = (page_no - 1) * page_size - return await UserCRUD(auth).page( + return await UserCRUD(self.auth).page( offset=offset, limit=page_size, order_by=order_by or [{"id": "asc"}], @@ -106,139 +70,82 @@ class UserService: out_schema=UserOutSchema, ) - @classmethod - async def create_service(cls, data: UserCreateSchema, auth: AuthSchema) -> UserOutSchema: - """ - 创建用户 - - 参数: - - data (UserCreateSchema): 用户创建信息 - - auth (AuthSchema): 认证信息模型 - - 返回: - - dict: 创建后的用户详情字典 - """ + async def create(self, data: UserCreateSchema) -> UserOutSchema: if not data.username: raise CustomException(msg="用户名不能为空") - # 检查是否试图创建超级管理员 if data.is_superuser: raise CustomException(msg="不允许创建超级管理员") - # 检查用户名是否存在 - user = await UserCRUD(auth).get(username=data.username) + user = await UserCRUD(self.auth).get(username=data.username) if user: raise CustomException(msg="已存在相同用户名称的账号") - # 检查部门是否存在 if data.dept_id: - dept = await DeptCRUD(auth).get(id=data.dept_id) + dept = await DeptCRUD(self.auth).get(id=data.dept_id) if not dept: raise CustomException(msg="该数据不存在") - # 检查租户配额 - await TenantService.check_quota_service(auth, auth.tenant_id, "user") + await TenantService(self.auth).check_quota(self.auth.tenant_id, "user") - # 创建用户 if data.password: data.password = PwdUtil.set_password_hash(password=data.password) user_dict = data.model_dump(exclude_unset=True, exclude={"role_ids", "position_ids"}) - # 创建用户 - new_user = await UserCRUD(auth).create(data=user_dict) - # 设置角色 + new_user = await UserCRUD(self.auth).create(data=user_dict) if data.role_ids and len(data.role_ids) > 0: - await UserCRUD(auth).set_user_roles(user_ids=[new_user.id], role_ids=data.role_ids) - # 设置岗位 + await UserCRUD(self.auth).set_user_roles(user_ids=[new_user.id], role_ids=data.role_ids) if data.position_ids and len(data.position_ids) > 0: - await UserCRUD(auth).set_user_positions(user_ids=[new_user.id], position_ids=data.position_ids) + await UserCRUD(self.auth).set_user_positions(user_ids=[new_user.id], position_ids=data.position_ids) + return UserOutSchema.model_validate(new_user) - new_user_dict = UserOutSchema.model_validate(new_user) - return new_user_dict - - @classmethod - async def update_service(cls, id: int, data: UserUpdateSchema, auth: AuthSchema) -> UserOutSchema: - """ - 更新用户 - - 参数: - - id (int): 用户ID - - data (UserUpdateSchema): 用户更新信息 - - auth (AuthSchema): 认证信息模型 - - 返回: - - Dict: 更新后的用户详情字典 - """ + async def update(self, id: int, data: UserUpdateSchema) -> UserOutSchema: if not data.username: raise CustomException(msg="账号不能为空") - # 检查用户是否存在 - user = await UserCRUD(auth).get_or_404(id=id) - - # 检查是否尝试修改超级管理员 + user = await UserCRUD(self.auth).get_or_404(id=id) if user.is_superuser: raise CustomException(msg="超级管理员不允许修改") - # 检查用户名是否重复 - exist_user = await UserCRUD(auth).get(username=data.username) + exist_user = await UserCRUD(self.auth).get(username=data.username) if exist_user and exist_user.id != id: raise CustomException(msg="更新失败,账号已存在") - # 新增:检查手机号是否重复 if data.mobile: - exist_mobile_user = await UserCRUD(auth).get(mobile=data.mobile) + exist_mobile_user = await UserCRUD(self.auth).get(mobile=data.mobile) if exist_mobile_user and exist_mobile_user.id != id: raise CustomException(msg="该数据已存在") - # 新增:检查邮箱是否重复 if data.email: - exist_email_user = await UserCRUD(auth).get(email=data.email) + exist_email_user = await UserCRUD(self.auth).get(email=data.email) if exist_email_user and exist_email_user.id != id: raise CustomException(msg="该数据已存在") - # 检查部门是否存在且可用 if data.dept_id: - dept = await DeptCRUD(auth).get(id=data.dept_id) + dept = await DeptCRUD(self.auth).get(id=data.dept_id) if not dept: raise CustomException(msg="该数据不存在") if dept.status == 1: raise CustomException(msg="部门已被禁用") - # 更新用户 - new_user = await UserCRUD(auth).update(id=id, data=data) + new_user = await UserCRUD(self.auth).update(id=id, data=data) - # 更新角色和岗位 if data.role_ids and len(data.role_ids) > 0: - # 检查角色是否都存在且可用 - roles = await RoleCRUD(auth).list(search={"id": ("in", data.role_ids)}) + roles = await RoleCRUD(self.auth).list(search={"id": ("in", data.role_ids)}) if len(roles) != len(data.role_ids): raise CustomException(msg="更新失败,部分角色不存在") if not all(role.status == 0 for role in roles): raise CustomException(msg="更新失败,部分角色已被禁用") - await UserCRUD(auth).set_user_roles(user_ids=[id], role_ids=data.role_ids) + await UserCRUD(self.auth).set_user_roles(user_ids=[id], role_ids=data.role_ids) if data.position_ids and len(data.position_ids) > 0: - # 检查岗位是否都存在且可用 - positions = await PositionCRUD(auth).list(search={"id": ("in", data.position_ids)}) + positions = await PositionCRUD(self.auth).list(search={"id": ("in", data.position_ids)}) if len(positions) != len(data.position_ids): raise CustomException(msg="更新失败,部分岗位不存在") if not all(position.status == 0 for position in positions): raise CustomException(msg="更新失败,部分岗位已被禁用") - await UserCRUD(auth).set_user_positions(user_ids=[id], position_ids=data.position_ids) + await UserCRUD(self.auth).set_user_positions(user_ids=[id], position_ids=data.position_ids) - user_dict = UserOutSchema.model_validate(new_user) - return user_dict + return UserOutSchema.model_validate(new_user) - @classmethod - async def delete_service(cls, auth: AuthSchema, ids: list[int]) -> None: - """ - 删除用户 - - 参数: - - auth (AuthSchema): 认证信息模型 - - ids (list[int]): 用户ID列表 - - 返回: - - None - """ + async def delete(self, ids: list[int]) -> None: if len(ids) < 1: raise CustomException(msg="删除失败,删除对象不能为空") - # 批量查询所有待删除用户 - users = await UserCRUD(auth).list(search={"id": ("in", ids)}) + users = await UserCRUD(self.auth).list(search={"id": ("in", ids)}) user_map = {u.id: u for u in users} for uid in ids: user = user_map.get(uid) @@ -248,62 +155,40 @@ class UserService: raise CustomException(msg="超级管理员不能删除") if user.status == 0: raise CustomException(msg="用户已启用,不能删除") - if auth.user and auth.user.id == uid: + if self.auth.user and self.auth.user.id == uid: raise CustomException(msg="不能删除当前登陆用户") - # 删除用户角色关联数据 - await UserCRUD(auth).set_user_roles(user_ids=ids, role_ids=[]) - # 删除用户岗位关联数据 - await UserCRUD(auth).set_user_positions(user_ids=ids, position_ids=[]) + await UserCRUD(self.auth).set_user_roles(user_ids=ids, role_ids=[]) + await UserCRUD(self.auth).set_user_positions(user_ids=ids, position_ids=[]) + await UserCRUD(self.auth).delete(ids=ids) - # 删除用户 - await UserCRUD(auth).delete(ids=ids) - - @classmethod - async def current_info_service(cls, auth: AuthSchema) -> UserOutSchema: - """ - 获取当前用户信息 - - 参数: - - auth (AuthSchema): 认证信息模型 - - 返回: - - Dict: 当前用户详情字典 - """ - # 获取用户基本信息 - if not auth.user or not auth.user.id: + async def current_info(self) -> UserOutSchema: + if not self.auth.user or not self.auth.user.id: raise CustomException(msg="该数据不存在") - user = await UserCRUD(auth).get(id=auth.user.id) + user = await UserCRUD(self.auth).get(id=self.auth.user.id) user_dict = UserOutSchema.model_validate(user) - # 获取部门名称 if user and user.dept: user_dict.dept_name = user.dept.name - # 获取菜单权限(PC 管理端侧栏:仅 client=pc) _pc_only = {"client": "pc"} - if auth.user and auth.user.is_superuser: - # 使用树形结构查询,预加载children关系(含 type=3 按钮,供前端权限列表使用) - menu_all = await MenuCRUD(auth).tree_list( + if self.auth.user and self.auth.user.is_superuser: + menu_all = await MenuCRUD(self.auth).tree_list( search={"type": ("in", [1, 2, 3, 4]), "status": 0, **_pc_only}, order_by=[{"order": "asc"}], ) menus = [MenuOutSchema.model_validate(menu) for menu in menu_all] - else: - # 收集用户所有角色的菜单 ID(含按钮,供前端权限列表使用) - menu_ids = {menu.id for role in auth.user.roles or [] for menu in role.menus if menu.status == 0 and getattr(menu, "client", "pc") == "pc"} + menu_ids = {menu.id for role in self.auth.user.roles or [] for menu in role.menus if menu.status == 0 and getattr(menu, "client", "pc") == "pc"} - # 租户菜单约束:非超管用户只能看到租户菜单权限内的菜单 - if menu_ids and auth.tenant_id: - allowed_ids = await PackageService.get_tenant_available_menu_ids(auth, auth.tenant_id) + if menu_ids and self.auth.tenant_id: + allowed_ids = await PackageService(self.auth).get_tenant_available_menu_ids(self.auth.tenant_id) allowed_set = set(allowed_ids) menu_ids = menu_ids & allowed_set - # 使用树形结构查询,预加载children关系 menus = ( [ MenuOutSchema.model_validate(menu) - for menu in await MenuCRUD(auth).tree_list( + for menu in await MenuCRUD(self.auth).tree_list( search={"id": ("in", list(menu_ids)), **_pc_only}, order_by=[{"order": "asc"}], ) @@ -314,129 +199,66 @@ class UserService: user_dict.menus = traversal_to_tree([menu.model_dump() for menu in menus]) return user_dict - @classmethod - async def update_current_info_service(cls, auth: AuthSchema, data: CurrentUserUpdateSchema) -> UserOutSchema: - """ - 更新当前用户信息 - - 参数: - - auth (AuthSchema): 认证信息模型 - - data (CurrentUserUpdateSchema): 当前用户更新信息 - - 返回: - - Dict: 更新后的当前用户详情字典 - """ - if not auth.user or not auth.user.id: + async def update_current_info(self, data: CurrentUserUpdateSchema) -> UserOutSchema: + if not self.auth.user or not self.auth.user.id: raise CustomException(msg="该数据不存在") - user = await UserCRUD(auth).get(id=auth.user.id) + user = await UserCRUD(self.auth).get(id=self.auth.user.id) if not user: raise CustomException(msg="该数据不存在") if user.is_superuser: raise CustomException(msg="超级管理员不能修改个人信息") - # 新增:检查手机号是否重复 if data.mobile: - exist_mobile_user = await UserCRUD(auth).get(mobile=data.mobile) - if exist_mobile_user and exist_mobile_user.id != auth.user.id: + exist_mobile_user = await UserCRUD(self.auth).get(mobile=data.mobile) + if exist_mobile_user and exist_mobile_user.id != self.auth.user.id: raise CustomException(msg="该数据已存在") - # 新增:检查邮箱是否重复 if data.email: - exist_email_user = await UserCRUD(auth).get(email=data.email) - if exist_email_user and exist_email_user.id != auth.user.id: + exist_email_user = await UserCRUD(self.auth).get(email=data.email) + if exist_email_user and exist_email_user.id != self.auth.user.id: raise CustomException(msg="该数据已存在") user_update_data = UserUpdateSchema(**data.model_dump()) - new_user = await UserCRUD(auth).update(id=auth.user.id, data=user_update_data) + new_user = await UserCRUD(self.auth).update(id=self.auth.user.id, data=user_update_data) return UserOutSchema.model_validate(new_user) - @classmethod - async def set_available_service(cls, auth: AuthSchema, data: BatchSetAvailable) -> None: - """ - 设置用户状态 - - 参数: - - auth (AuthSchema): 认证信息模型 - - data (BatchSetAvailable): 批量设置用户状态数据 - - 返回: - - None - """ - for id in data.ids: - user = await UserCRUD(auth).get_or_404(id=id) + async def set_available(self, data: BatchSetAvailable) -> None: + for mid in data.ids: + user = await UserCRUD(self.auth).get_or_404(id=mid) if user.is_superuser: raise CustomException(msg="超级管理员状态不能修改") - await UserCRUD(auth).set(ids=data.ids, status=data.status) + await UserCRUD(self.auth).set(ids=data.ids, status=data.status) - @classmethod - async def change_password_service(cls, auth: AuthSchema, data: UserChangePasswordSchema) -> UserOutSchema: - """ - 修改用户密码 - - 参数: - - auth (AuthSchema): 认证信息模型 - - data (UserChangePasswordSchema): 用户密码修改数据 - - 返回: - - Dict: 更新后的当前用户详情字典 - """ - if not auth.user or not auth.user.id: + async def change_password(self, data: UserChangePasswordSchema) -> UserOutSchema: + if not self.auth.user or not self.auth.user.id: raise CustomException(msg="该数据不存在") if not data.old_password or not data.new_password: raise CustomException(msg="密码不能为空") - # 验证原密码 - user = await UserCRUD(auth).get(id=auth.user.id) + user = await UserCRUD(self.auth).get(id=self.auth.user.id) if not user: raise CustomException(msg="该数据不存在") if not PwdUtil.verify_password(plain_password=data.old_password, password_hash=user.password): raise CustomException(msg="原密码输入错误") - # 更新密码 new_password_hash = PwdUtil.set_password_hash(password=data.new_password) - new_user = await UserCRUD(auth).change_password(id=user.id, password_hash=new_password_hash) + new_user = await UserCRUD(self.auth).change_password(id=user.id, password_hash=new_password_hash) return UserOutSchema.model_validate(new_user) - @classmethod - async def reset_password_service(cls, auth: AuthSchema, data: ResetPasswordSchema) -> UserOutSchema: - """ - 重置用户密码 - - 参数: - - auth (AuthSchema): 认证信息模型 - - data (ResetPasswordSchema): 用户密码重置数据 - - 返回: - - Dict: 更新后的当前用户详情字典 - """ + async def reset_password(self, data: ResetPasswordSchema) -> UserOutSchema: if not data.password: raise CustomException(msg="密码不能为空") - # 验证用户 - user = await UserCRUD(auth).get(id=data.id) + user = await UserCRUD(self.auth).get(id=data.id) if not user: raise CustomException(msg="该数据不存在") - # 检查是否是超级管理员 if user.is_superuser: raise CustomException(msg="超级管理员密码不能重置") - # 更新密码 new_password_hash = PwdUtil.set_password_hash(password=data.password) - new_user = await UserCRUD(auth).change_password(id=data.id, password_hash=new_password_hash) + new_user = await UserCRUD(self.auth).change_password(id=data.id, password_hash=new_password_hash) return UserOutSchema.model_validate(new_user) - @classmethod - async def register_service(cls, auth: AuthSchema, data: UserRegisterSchema) -> UserOutSchema: - """ - 用户注册 - - 参数: - - auth (AuthSchema): 认证信息模型 - - data (UserRegisterSchema): 用户注册数据 - - 返回: - - Dict: 注册后的用户详情字典 - """ - # 检查用户名是否存在 - username_ok = await UserCRUD(auth).get(username=data.username) + async def register(self, data: UserRegisterSchema) -> UserOutSchema: + username_ok = await UserCRUD(self.auth).get(username=data.username) if username_ok: raise CustomException(msg="该数据已存在") @@ -444,59 +266,32 @@ class UserService: data.name = data.username create_dict = data.model_dump(exclude_unset=True, exclude={"role_ids", "position_ids"}) - # 设置创建人ID - if auth.user and auth.user.id: - create_dict["created_id"] = auth.user.id + if self.auth.user and self.auth.user.id: + create_dict["created_id"] = self.auth.user.id - result = await UserCRUD(auth).create(data=create_dict) + result = await UserCRUD(self.auth).create(data=create_dict) if data.role_ids: - await UserCRUD(auth).set_user_roles(user_ids=[result.id], role_ids=data.role_ids) + await UserCRUD(self.auth).set_user_roles(user_ids=[result.id], role_ids=data.role_ids) return UserOutSchema.model_validate(result) - @classmethod - async def forget_password_service(cls, auth: AuthSchema, data: UserForgetPasswordSchema) -> UserOutSchema: - """ - 用户忘记密码 - - 参数: - - auth (AuthSchema): 认证信息模型 - - data (UserForgetPasswordSchema): 用户忘记密码数据 - - 返回: - - Dict: 更新后的当前用户详情字典 - """ - user = await UserCRUD(auth).get(username=data.username) + async def forget_password(self, data: UserForgetPasswordSchema) -> UserOutSchema: + user = await UserCRUD(self.auth).get(username=data.username) if not user: raise CustomException(msg="该数据不存在") if user.status == 1: raise CustomException(msg="用户已停用") - # 检查是否是超级管理员 if user.is_superuser: raise CustomException(msg="超级管理员密码不能重置") - # 校验手机号是否匹配 if data.mobile and user.mobile != data.mobile: raise CustomException(msg="手机号不匹配") new_password_hash = PwdUtil.set_password_hash(password=data.new_password) - new_user = await UserCRUD(auth).forget_password(id=user.id, password_hash=new_password_hash) + new_user = await UserCRUD(self.auth).forget_password(id=user.id, password_hash=new_password_hash) return UserOutSchema.model_validate(new_user) - @classmethod - async def batch_import_service(cls, auth: AuthSchema, file: UploadFile, update_support: bool = False) -> str: - """ - 批量导入用户 - - 参数: - - auth (AuthSchema): 认证信息模型 - - file (UploadFile): 上传的Excel文件 - - update_support (bool, optional): 是否支持更新已存在用户. 默认值为False. - - 返回: - - str: 导入结果消息 - """ - + async def batch_import(self, file: UploadFile, update_support: bool = False) -> str: header_dict = { "部门编号": "dept_id", "账号": "username", @@ -508,7 +303,6 @@ class UserService: } try: - # 读取Excel文件 contents = await file.read() df = pd.read_excel(io.BytesIO(contents)) await file.close() @@ -516,79 +310,72 @@ class UserService: if df.empty: raise CustomException(msg="导入文件为空") - # 检查表头是否完整 missing_headers = [header for header in header_dict if header not in df.columns] if missing_headers: raise CustomException(msg=f"导入文件缺少必要的列: {', '.join(missing_headers)}") - # 重命名列名 df.rename(columns=header_dict, inplace=True) - # 验证必填字段 required_fields = ["username", "name", "dept_id"] errors = [] for field in required_fields: - missing_rows = df[df[field].isnull()].index.tolist() - if missing_rows: - field_name = next(k for k, v in header_dict.items() if v == field) - rows_str = "、".join([str(i + 1) for i in missing_rows]) - errors.append(f"{field_name}不能为空,第{rows_str}行") + if df[field].isnull().any(): + missing_count = df[field].isnull().sum() + errors.append(f"字段'{field}'有{missing_count}行缺少数据") if errors: - raise CustomException(msg=";".join(errors)) + raise CustomException(msg="\n".join(errors)) - error_msgs = [] success_count = 0 - count = 0 + error_msgs = [] - # 处理每一行数据 - for _index, row in df.iterrows(): + for i, (_, row) in enumerate(df.iterrows(), start=2): try: - count = count + 1 - # 数据转换 - gender = "0" if row["gender"] == "男" else ("1" if row["gender"] == "女" else "2") - status = 0 if row["status"] == "正常" else 1 + username = str(row["username"]).strip() if pd.notna(row["username"]) else "" + name = str(row["name"]).strip() if pd.notna(row["name"]) else "" + if not username: + error_msgs.append(f"第{i}行: 账号不能为空") + continue + if not name: + error_msgs.append(f"第{i}行: 昵称不能为空") + continue - # 构建用户数据 user_data = { - "username": str(row["username"]).strip(), - "name": str(row["name"]).strip(), - "email": str(row["email"]).strip(), - "mobile": str(row["mobile"]).strip(), - "gender": gender, - "status": status, + "username": username, + "name": name, + "email": str(row["email"]).strip() if pd.notna(row["email"]) else None, + "mobile": str(row["mobile"]).strip() if pd.notna(row["mobile"]) else None, + "gender": str(row["gender"]).strip() if pd.notna(row["gender"]) else "1", + "status": 0 if str(row["status"]).strip() == "正常" else 1, "dept_id": int(row["dept_id"]), - "password": PwdUtil.set_password_hash(password="123456"), # 设置默认密码 + "password": PwdUtil.set_password_hash(password="123456"), } - # 处理用户导入 - exists_user = await UserCRUD(auth).get(username=user_data["username"]) + exists_user = await UserCRUD(self.auth).get(username=user_data["username"]) if exists_user: - # 检查是否是超级管理员 if exists_user.is_superuser: - error_msgs.append(f"第{count}行: 超级管理员不允许修改") + error_msgs.append(f"第{i}行: 超级管理员不允许修改") continue if update_support: user_update_data = UserUpdateSchema(**user_data) - await UserCRUD(auth).update(id=exists_user.id, data=user_update_data) + await UserCRUD(self.auth).update(id=exists_user.id, data=user_update_data) success_count += 1 else: - error_msgs.append(f"第{count}行: 用户 {user_data['username']} 已存在") + error_msgs.append(f"第{i}行: 用户 {user_data['username']} 已存在") else: user_create_schema = UserCreateSchema(**user_data) user_create_data = user_create_schema.model_dump(exclude_unset=True, exclude={"role_ids", "position_ids"}) - new_user = await UserCRUD(auth).create(data=user_create_data) + new_user = await UserCRUD(self.auth).create(data=user_create_data) if user_create_schema.role_ids and len(user_create_schema.role_ids) > 0: - await UserCRUD(auth).set_user_roles(user_ids=[new_user.id], role_ids=user_create_schema.role_ids) + await UserCRUD(self.auth).set_user_roles(user_ids=[new_user.id], role_ids=user_create_schema.role_ids) if user_create_schema.position_ids and len(user_create_schema.position_ids) > 0: - await UserCRUD(auth).set_user_positions(user_ids=[new_user.id], position_ids=user_create_schema.position_ids) + await UserCRUD(self.auth).set_user_positions(user_ids=[new_user.id], position_ids=user_create_schema.position_ids) success_count += 1 except Exception as e: - error_msgs.append(f"第{count}行: 异常{e!s}") + error_msgs.append(f"第{i}行: 异常{e!s}") continue - # 返回详细的导入结果 result = f"成功导入 {success_count} 条数据" if error_msgs: result += "\n错误信息:\n" + "\n".join(error_msgs) @@ -598,14 +385,8 @@ class UserService: logger.error(f"批量导入用户失败: {e!s}") raise CustomException(msg=f"导入失败: {e!s}") from e - @classmethod - async def get_import_template_user_service(cls) -> bytes: - """ - 获取用户导入模板 - - 返回: - - bytes: Excel文件字节流 - """ + @staticmethod + def get_import_template() -> bytes: header_list = [ "部门编号", "账号", @@ -626,21 +407,11 @@ class UserService: option_list=option_list, ) - @classmethod - async def export_list_service(cls, user_list: list[dict[str, Any]]) -> bytes: - """ - 导出用户列表为Excel文件 - - 参数: - - user_list (list[dict[str, Any]]): 用户列表 - - 返回: - - bytes: Excel文件字节流 - """ + @staticmethod + def export_list(user_list: list[dict[str, Any]]) -> bytes: if not user_list: raise CustomException(msg="没有数据可导出") - # 定义字段映射 mapping_dict = { "id": "用户编号", "avatar": "头像", @@ -659,8 +430,6 @@ class UserService: "updated_id": "更新者ID", } - # 复制数据并转换 - # creator = {'id': 1, 'name': '管理员', 'username': 'admin'} data = user_list.copy() for item in data: item["status"] = "启用" if item.get("status") == 0 else "停用" diff --git a/backend/app/common/enums.py b/backend/app/common/enums.py index 2ba2916a..ae41a200 100644 --- a/backend/app/common/enums.py +++ b/backend/app/common/enums.py @@ -114,14 +114,16 @@ class PermissionFilterStrategy(str, Enum): """ 权限过滤策略枚举 - 定义不同的权限过滤策略,让模型选择合适的过滤方式 + 每个策略对应一种过滤实现,模型通过 ``__permission_strategy__`` 选择。 + 注意:``DATA_SCOPE`` 是 dispatcher(基于 ``data_scope`` 字段再分发到 + 5 个具体的 data_scope 子策略),其余是具体策略。 """ - DATA_SCOPE = "data_scope" # 基于数据范围权限(默认) - ROLE_BASED = "role_based" # 基于角色授权(菜单) - DEPT_BASED = "dept_based" # 基于部门关联(部门、角色) - SELF_ONLY = "self_only" # 仅本人数据 - USER_ROLE = "user_role" # 当前用户绑定的角色 + DATA_SCOPE = "data_scope" # 数据范围权限分发器(默认) + MENU_AUTH = "menu_auth" # 菜单授权(用于 MenuModel,按角色-菜单绑定过滤) + DEPT_RELATION = "dept_relation" # 部门关联(用于 DeptModel、RoleModel,按所属部门过滤) + OWN = "own" # 仅本人数据 + USER_BINDING = "user_binding" # 用户绑定角色(用于 RoleModel,仅显示当前用户绑定的角色) @unique diff --git a/backend/app/common/response.py b/backend/app/common/response.py index b38418fe..6f49f901 100644 --- a/backend/app/common/response.py +++ b/backend/app/common/response.py @@ -1,12 +1,11 @@ from collections.abc import Mapping from datetime import date, datetime, time -from typing import Any, Generic +from typing import Any from fastapi import status from fastapi.encoders import jsonable_encoder from fastapi.responses import FileResponse, JSONResponse, StreamingResponse from pydantic import BaseModel, Field -from pydantic.types import T from starlette.background import BackgroundTask from app.common.constant import DATE_DISPLAY_FMT, DATETIME_DISPLAY_FMT, RET, TIME_DISPLAY_FMT @@ -23,7 +22,7 @@ def jsonable_response_content(content: Any) -> Any: return jsonable_encoder(content, custom_encoder=_JSON_DATETIME_CUSTOM_ENCODER) -class ResponseSchema(BaseModel, Generic[T]): +class ResponseSchema[T](BaseModel): """响应模型""" code: int = Field(default=RET.OK.code, description="业务状态码") diff --git a/backend/app/config/setting.py b/backend/app/config/setting.py index b7320c3c..eee86d5d 100755 --- a/backend/app/config/setting.py +++ b/backend/app/config/setting.py @@ -68,7 +68,7 @@ class Settings(BaseSettings): ALGORITHM: str = "HS256" # JWT算法 ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 60 * 12 # access_token过期时间(秒)12 小时 REFRESH_TOKEN_EXPIRE_MINUTES: int = 60 * 60 * 12 # refresh_token过期时间(秒)12 小时 - TOKEN_TYPE: str = "bearer" # token类型 + TOKEN_TYPE: str = "Bearer" # token类型(RFC 6750 标准大小写) TOKEN_REQUEST_PATH_EXCLUDE: list[str] = ["api/v1/auth/login"] # JWT / RBAC 路由白名单 TOKEN_SLIDING_EXPIRE: bool = True # 是否启用滑动过期(用户操作时自动续期) @@ -113,8 +113,9 @@ class Settings(BaseSettings): POOL_USE_LIFO: bool = True # 是否使用LIFO连接池 POOL_PRE_PING: bool = True # 是否开启连接预检 FUTURE: bool = True # 是否使用SQLAlchemy 2.0特性 - AUTOCOMMIT: bool = False # 是否自动提交 - AUTOFETCH: bool = False # 是否自动刷新 + AUTOCOMMIT: bool = False # 是否自动提交(映射 SQLAlchemy sessionmaker(autocommit=...)) + AUTOFLUSH: bool = False # 是否自动刷新(映射 SQLAlchemy sessionmaker(autoflush=...)) + AUTOFETCH: bool | None = None # 兼容旧环境变量名(保留别名,避免 .env 中已有 AUTOFETCH 的部署报错) EXPIRE_ON_COMMIT: bool = False # 是否在提交时过期 # MySQL/PostgreSQL数据库连接 @@ -247,18 +248,16 @@ class Settings(BaseSettings): # ******************* 请求限制配置 ****************** # # ================================================= # REQUEST_LIMITER_REDIS_PREFIX: str = "fastapiadmin:request_limiter:" + RATE_LIMITER_TIMES: int = 200 + RATE_LIMITER_SECONDS: int = 10 + WS_RATE_LIMITER_TIMES: int = 1 + WS_RATE_LIMITER_SECONDS: int = 5 # ================================================= # # ******************* 重构配置 ******************* # # ================================================= # @property def MIDDLEWARE_LIST(self) -> list[str | None]: - """ - 根据开关组装的中间件类路径列表(未启用的项为 None)。 - - 返回: - - list[str | None]: 中间件 import 路径或 None。 - """ # 中间件列表(注册时逆序叠加:下列第一项在列表中最前,最终位于最外层,优先生效) # 中间件执行顺序(从外到内): # CORS → RequestLog → SecurityHeaders → GZip → CorrelationId → 业务路由 @@ -274,12 +273,6 @@ class Settings(BaseSettings): @property def EVENT_LIST(self) -> list[str | None]: - """ - 应用启动时加载的全局异步事件模块路径列表。 - - 返回: - - list[str | None]: 事件模块路径或 None。 - """ EVENTS: list[str | None] = [ "app.core.database.redis_connect" if self.REDIS_ENABLE else None, ] @@ -287,15 +280,6 @@ class Settings(BaseSettings): @property def ASYNC_DB_URI(self) -> str: - """ - 异步 SQLAlchemy 数据库 URL。 - - 返回: - - str: 异步驱动连接串。 - - 异常: - - ValueError: 数据库类型不支持时抛出。 - """ if self.DATABASE_TYPE not in ("mysql", "postgres", "sqlite"): raise ValueError( f"数据库驱动不支持: {self.DATABASE_TYPE}, 异步数据库请选择 mysql、postgres、sqlite" @@ -311,15 +295,6 @@ class Settings(BaseSettings): @property def DB_URI(self) -> str: - """ - 同步 SQLAlchemy 数据库 URL。 - - 返回: - - str: 同步驱动连接串。 - - 异常: - - ValueError: 数据库类型不支持时抛出。 - """ if self.DATABASE_TYPE not in ("mysql", "postgres", "sqlite"): raise ValueError( f"数据库驱动不支持: {self.DATABASE_TYPE}, 同步数据库请选择 mysql、postgres、sqlite" @@ -335,22 +310,10 @@ class Settings(BaseSettings): @property def REDIS_URI(self) -> str: - """ - Redis 连接 URL。 - - 返回: - - str: redis:// 连接串。 - """ return f"redis://{self.REDIS_USER}:{self.REDIS_PASSWORD}@{self.REDIS_HOST}:{self.REDIS_PORT}/{self.REDIS_DB_NAME}" @property def FASTAPI_CONFIG(self) -> dict[str, Any]: - """ - 创建 FastAPI 应用实例时使用的关键字参数子集。 - - 返回: - - dict[str, Any]: debug、title、responses 等配置。 - """ return { "debug": self.DEBUG, "title": self.TITLE, @@ -374,12 +337,6 @@ class Settings(BaseSettings): @lru_cache(maxsize=1) def get_settings() -> Settings: - """ - 获取全局 Settings 单例(lru_cache 缓存)。 - - 返回: - - Settings: 配置实例。 - """ return Settings() diff --git a/backend/app/core/base_crud.py b/backend/app/core/base_crud.py index 8c85cdb6..ce15122f 100644 --- a/backend/app/core/base_crud.py +++ b/backend/app/core/base_crud.py @@ -18,7 +18,7 @@ import builtins from collections.abc import Sequence from datetime import datetime, timedelta -from typing import TYPE_CHECKING, Any, Generic, TypeVar +from typing import TYPE_CHECKING, Any, TypeVar from pydantic import BaseModel from sqlalchemy import Select, asc, delete, desc, false, func, literal_column, select, update @@ -29,20 +29,16 @@ from sqlalchemy.sql.elements import ColumnElement from app.core.base_model import MappedBase from app.core.base_schema import AuthSchema, PageResultSchema -from app.core.database import async_db_session from app.core.exceptions import CustomException from app.core.permission import Permission if TYPE_CHECKING: from sqlalchemy.engine import Result -ModelType = TypeVar("ModelType", bound=MappedBase) -CreateSchemaType = TypeVar("CreateSchemaType", bound=BaseModel) -UpdateSchemaType = TypeVar("UpdateSchemaType", bound=BaseModel) OutSchemaType = TypeVar("OutSchemaType", bound=BaseModel) -class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]): +class CRUDBase[ModelType: MappedBase, CreateSchemaType: BaseModel, UpdateSchemaType: BaseModel]: """统一数据层基类 — 有认证时自动处理权限,无认证时纯数据操作""" def __init__( @@ -91,14 +87,12 @@ class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]): data["deleted_id"] = self.auth.user.id return data - # ── 查询 ── async def _get_one( - self, session: AsyncSession, preload: list[str | Any] | None = None, **kwargs + self, preload: list[str | Any] | None = None, **kwargs ) -> ModelType | None: - """内部方法:在指定会话上执行单条查询(get / update 共用) + """内部方法:在当前实例会话上执行单条查询(get / update 共用) 参数: - - session: 数据库会话(只读 or 事务) - preload: 预加载关系 - **kwargs: 查询条件 @@ -110,12 +104,12 @@ class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]): for opt in self.__loader_options(preload): sql = sql.options(opt) sql = await self.__filter_permissions(sql) - result: Result = await session.execute(sql) + result: Result = await self.db.execute(sql) return result.scalars().first() async def get(self, preload: list[str | Any] | None = None, **kwargs) -> ModelType | None: """ - 根据条件获取单个对象(使用只读会话,不参与请求事务) + 根据条件获取单个对象(复用请求级事务会话,保证读已写一致性) 参数: - preload: 预加载关系 @@ -125,8 +119,7 @@ class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]): - 对象实例或 None """ try: - async with async_db_session() as rdb: - return await self._get_one(rdb, preload=preload, **kwargs) + return await self._get_one(preload=preload, **kwargs) except CustomException: raise except Exception as e: @@ -181,7 +174,7 @@ class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]): async def count(self, **kwargs) -> int: """ - 统计符合条件的记录数(使用只读会话,不参与请求事务) + 统计符合条件的记录数(复用请求级事务会话) 参数: - **kwargs: 查询条件,支持元组语法 @@ -193,8 +186,7 @@ class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]): conditions = await self.__build_conditions(**kwargs) count_sql = select(func.count()).select_from(self.model).where(*conditions) count_sql = await self.__filter_permissions(count_sql) - async with async_db_session() as rdb: - result = await rdb.execute(count_sql) + result: Result = await self.db.execute(count_sql) return result.scalar() or 0 except CustomException: raise @@ -208,7 +200,7 @@ class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]): preload: list[str | Any] | None = None, ) -> Sequence[ModelType]: """ - 根据条件获取对象列表(使用只读会话,不参与请求事务) + 根据条件获取对象列表(复用请求级事务会话) 参数: - search: 查询条件 @@ -225,8 +217,7 @@ class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]): for opt in self.__loader_options(preload): sql = sql.options(opt) sql = await self.__filter_permissions(sql) - async with async_db_session() as rdb: - result: Result = await rdb.execute(sql) + result: Result = await self.db.execute(sql) return result.scalars().all() except CustomException: raise @@ -241,7 +232,7 @@ class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]): preload: builtins.list[str | Any] | None = None, ) -> Sequence[ModelType]: """ - 获取树形结构数据列表(使用只读会话,不参与请求事务) + 获取树形结构数据列表(复用请求级事务会话) 参数: - search: 查询条件 @@ -269,8 +260,7 @@ class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]): sql = sql.options(opt) sql = await self.__filter_permissions(sql) - async with async_db_session() as rdb: - result: Result = await rdb.execute(sql) + result: Result = await self.db.execute(sql) return result.scalars().all() except CustomException: raise @@ -287,7 +277,7 @@ class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]): preload: builtins.list[str | Any] | None = None, ) -> PageResultSchema: """ - 获取分页数据(使用只读会话,不参与请求事务;count 与 data 共享同一会话) + 获取分页数据(复用请求级事务会话;count 与 data 共享同一会话) 参数: - offset: 偏移量 @@ -318,14 +308,13 @@ class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]): if where_clause is not None: count_sql = count_sql.where(where_clause) - async with async_db_session() as rdb: - total_result = await rdb.execute(count_sql) - total = total_result.scalar() or 0 + total_result = await self.db.execute(count_sql) + total = total_result.scalar() or 0 - result: Result = await rdb.execute( - data_sql.order_by(*self._parse_order(order)).offset(offset).limit(limit) - ) - objs = result.scalars().all() + result: Result = await self.db.execute( + data_sql.order_by(*self._parse_order(order)).offset(offset).limit(limit) + ) + objs = result.scalars().all() items = ( [out_schema.model_validate(obj).model_dump() for obj in objs] @@ -345,7 +334,6 @@ class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]): except Exception as e: raise CustomException(msg=f"分页查询失败: {e!s}") - # ── 创建 ── async def create(self, data: CreateSchemaType) -> ModelType: """创建新对象(有认证时自动填充租户与审计字段) @@ -380,7 +368,6 @@ class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]): except Exception as e: raise CustomException(msg=f"创建失败: {e!s}") - # ── 更新 ── async def update(self, id: int, data: UpdateSchemaType) -> ModelType: """更新对象(有认证时检查租户归属 + 填充审计字段) @@ -396,7 +383,7 @@ class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]): try: obj_dict = data.model_dump(exclude_unset=True, exclude={"id"}) model_defaults = getattr(self.model, "__loader_options__", []) - obj = await self._get_one(self.db, id=id, preload=model_defaults) + obj = await self._get_one(id=id, preload=model_defaults) if not obj: raise CustomException(msg="更新对象不存在") @@ -426,28 +413,20 @@ class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]): except Exception as e: raise CustomException(msg=f"更新失败: {e!s}") - # ── 删除 ── async def delete(self, ids: builtins.list[int]) -> None: - """软删除对象(有认证时加租户隔离 + 填充删除人) - - 事务由 request 级 db_getter 统一管理,本方法不开启独立事务。 - - 参数: - - ids: 对象 ID 列表 - """ + """软删除对象(有认证时填充删除人 + 租户隔离)""" try: pk = self._get_pk_col() - tenant_conds = self.__tenant_condition() if self._supports_soft_delete: - sql = ( - update(self.model) - .where(pk.in_(ids), *tenant_conds) - .values(**self._soft_delete_values()) - ) + sql = self._tenant_dml_where( + update(self.model).where(pk.in_(ids)) + ).values(**self._soft_delete_values()) await self.db.execute(sql) else: - sql = delete(self.model).where(pk.in_(ids), *tenant_conds) + sql = self._tenant_dml_where( + delete(self.model).where(pk.in_(ids)) + ) await self.db.execute(sql) await self.db.flush() except CustomException: @@ -456,18 +435,13 @@ class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]): raise CustomException(msg=f"删除失败: {e!s}") async def clear(self) -> None: - """软清空对象表(有认证时租户隔离) - - 事务由 request 级 db_getter 统一管理,本方法不开启独立事务。 - """ + """软清空对象表(有认证时填充删除人 + 租户隔离)""" try: - tenant_conds = self.__tenant_condition() - if self._supports_soft_delete: - sql = update(self.model).where(*tenant_conds).values(**self._soft_delete_values()) + sql = self._tenant_dml_where(update(self.model)).values(**self._soft_delete_values()) await self.db.execute(sql) else: - sql = delete(self.model).where(*tenant_conds) + sql = self._tenant_dml_where(delete(self.model)) await self.db.execute(sql) await self.db.flush() except CustomException: @@ -475,23 +449,11 @@ class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]): except Exception as e: raise CustomException(msg=f"清空失败: {e!s}") - # ── 批量操作 ── async def set(self, ids: builtins.list[int], **kwargs) -> None: - """批量更新字段(有认证时租户隔离) - - 事务由 request 级 db_getter 统一管理,本方法不开启独立事务。 - - 参数: - - ids: 对象 ID 列表 - - **kwargs: 更新属性及值 - """ + """批量更新字段(带租户隔离)""" try: pk = self._get_pk_col() - sql = ( - update(self.model) - .where(pk.in_(ids), *self.__tenant_condition()) - .values(**kwargs) - ) + sql = self._tenant_dml_where(update(self.model)).where(pk.in_(ids)).values(**kwargs) await self.db.execute(sql) await self.db.flush() except CustomException: @@ -500,22 +462,14 @@ class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]): raise CustomException(msg=f"批量更新失败: {e!s}") async def restore(self, ids: builtins.list[int]) -> None: - """恢复软删除对象(有认证时租户隔离) - - 事务由 request 级 db_getter 统一管理,本方法不开启独立事务。 - - 参数: - - ids: 对象 ID 列表 - """ + """恢复软删除对象(带租户隔离)""" try: if not self._supports_soft_delete: raise CustomException(msg="该模型不支持软删除,无法恢复") pk = self._get_pk_col() - sql = ( - update(self.model) - .where(pk.in_(ids), *self.__tenant_condition()) - .values(is_deleted=False, deleted_time=None, deleted_id=None) - ) + sql = self._tenant_dml_where( + update(self.model).where(pk.in_(ids)) + ).values(is_deleted=False, deleted_time=None, deleted_id=None) await self.db.execute(sql) await self.db.flush() except CustomException: @@ -523,25 +477,6 @@ class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]): except Exception as e: raise CustomException(msg=f"恢复失败: {e!s}") - # ── 内部方法 ── - def __tenant_condition(self) -> builtins.list[ColumnElement]: - """ - 获取租户隔离条件(无认证时返回空列表) - - 返回: - - 租户过滤条件列表 - """ - if not self.auth or not self.auth.user: - return [] - if self.auth.user.is_superuser: - return [] - if not hasattr(self.model, "tenant_id"): - return [] - tid = self.auth.user.tenant_id - if tid is not None: - return [getattr(self.model, "tenant_id") == tid] - return [] - async def __filter_permissions(self, sql: Select) -> Select: """过滤数据权限(仅用于 Select)""" if not self.auth: @@ -553,7 +488,6 @@ class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]): return await filter_obj.filter_query(sql) def _platform_shared_conditions(self) -> builtins.list[ColumnElement]: - """平台共享模型读取条件(租户可读 tenant_id=平台或自己的数据)""" if not self.auth or not self.auth.user: return [] tid = self.auth.user.tenant_id @@ -564,21 +498,28 @@ class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]): ] return [] + def _tenant_dml_where(self, sql): + """为 DML 语句注入 tenant_id 条件(不读平台数据)""" + if self.auth and hasattr(self.model, "tenant_id") \ + and self.auth.user and not self.auth.user.is_superuser: + tid = self.auth.tenant_id + if tid is not None: + return sql.where(getattr(self.model, "tenant_id") == tid) + return sql + async def __build_conditions(self, **kwargs) -> builtins.list[ColumnElement]: - """ - 构建查询条件(自动添加 is_deleted 过滤) - - 参数: - - **kwargs: 查询参数,支持元组语法: ("like", val), ("in", []), ("between", [a,b]) 等 - - 返回: - - SQL 条件表达式列表 - """ conditions: builtins.list[ColumnElement] = [] if hasattr(self.model, "is_deleted"): conditions.append(getattr(self.model, "is_deleted") == False) # noqa: E712 + if self.auth and hasattr(self.model, "tenant_id") \ + and not getattr(self.model, "__platform_data_shared__", False): + if self.auth.user and not self.auth.user.is_superuser: + tid = self.auth.tenant_id + if tid is not None: + conditions.append(getattr(self.model, "tenant_id") == tid) + for key, value in kwargs.items(): if value is None or value == "": continue diff --git a/backend/app/core/base_schema.py b/backend/app/core/base_schema.py index ef2c3347..57019357 100644 --- a/backend/app/core/base_schema.py +++ b/backend/app/core/base_schema.py @@ -1,11 +1,16 @@ from datetime import datetime -from typing import Any, Generic, TypeVar +from typing import TYPE_CHECKING, Any, TypeVar from pydantic import BaseModel, ConfigDict, Field, model_validator +from sqlalchemy.ext.asyncio import AsyncSession from app.core.validator import DateTimeStr -T = TypeVar("T") +if TYPE_CHECKING: + # 仅供 IDE 类型推断,运行时不会触发导入,避免 app.core ↔ app.api.v1 循环依赖 + from app.api.v1.module_system.user.model import UserModel + +UserT = TypeVar("UserT") class CommonSchema(BaseModel): @@ -84,15 +89,27 @@ class BatchDelete(BaseModel): class AuthSchema(BaseModel): - """权限认证模型""" + """权限认证模型 + + ``user`` 字段运行时为 ``Any``(避免与 SQLAlchemy 懒加载冲突,也避免 + 循环依赖)。通过 ``get_user()`` 方法获得 IDE 类型推断。 + """ model_config = ConfigDict(arbitrary_types_allowed=True) - user: Any = Field(default=None, description="用户信息", exclude=True) + user: Any = Field(default=None, description="用户信息(UserModel 实例)", exclude=True) check_data_scope: bool = Field(default=True, description="是否检查数据权限") - db: Any = Field(default=None, description="数据库会话", exclude=True) + db: AsyncSession | None = Field(default=None, description="数据库会话", exclude=True) tenant_id: int | None = Field(default=None, description="租户ID,用于用户认证前查询") + def get_user(self) -> "UserModel | None": + """类型化的用户访问方法。 + + 业务方 ``auth.get_user()`` 由 IDE 推断为 ``UserModel | None``, + 享受自动补全。``auth.user`` 仍可用但无类型补全。 + """ + return self.user # type: ignore[return-value] + class JWTPayloadSchema(BaseModel): """JWT载荷模型""" @@ -131,7 +148,7 @@ class LogoutPayloadSchema(BaseModel): token: str = Field(..., min_length=1, description="token") -class PageResultSchema(BaseModel, Generic[T]): +class PageResultSchema[T](BaseModel): """分页查询结果模型""" model_config = ConfigDict(from_attributes=True) diff --git a/backend/app/core/database.py b/backend/app/core/database.py index b6d2d744..dfb13871 100644 --- a/backend/app/core/database.py +++ b/backend/app/core/database.py @@ -96,10 +96,10 @@ def create_async_engine_and_session( raise else: # 异步数据库会话工厂 - AsyncSessionLocal = async_sessionmaker( + AsyncSessionLocal = async_sessionmaker[AsyncSession]( bind=async_engine, autocommit=settings.AUTOCOMMIT, - autoflush=settings.AUTOFETCH, + autoflush=settings.AUTOFLUSH if settings.AUTOFETCH is None else settings.AUTOFETCH, expire_on_commit=settings.EXPIRE_ON_COMMIT, class_=AsyncSession, ) diff --git a/backend/app/core/dependencies.py b/backend/app/core/dependencies.py index 8ce6eda8..7b9db125 100644 --- a/backend/app/core/dependencies.py +++ b/backend/app/core/dependencies.py @@ -1,6 +1,7 @@ import json import time from collections.abc import AsyncGenerator +from dataclasses import replace from fastapi import Depends, Query, Request from redis.asyncio.client import Redis @@ -15,8 +16,9 @@ from app.core.database import async_db_session from app.core.exceptions import CustomException from app.core.logger import logger from app.core.redis_crud import RedisCURD +from app.core.request_context import RequestContext +from app.core.request_context import get_current_tenant_id as _get_ctx_tenant_id from app.core.security import OAuth2Schema, decode_access_token -from app.core.tenant import get_current_tenant_id as _get_ctx_tenant_id # 套餐菜单权限缓存: {tenant_id: (timestamp, [menu_ids])} _package_menu_cache: dict[int, tuple[float, list[int]]] = {} @@ -25,8 +27,8 @@ _package_menu_cache: dict[int, tuple[float, list[int]]] = {} async def db_getter() -> AsyncGenerator[AsyncSession, None]: """数据库会话 — 请求级生命周期管理。 - 一个 HTTP 请求内所有写入共享同一个事务,要么全成功,要么全失败。 - 自动提交(无异常时)或回滚(异常时)。 + 一个 HTTP 请求内所有 SQL 共享同一个事务:要么全成功,要么全失败。 + 读操作也走这个事务(牺牲一点 MVCC 隔离换取读已写一致性)。 """ async with async_db_session() as session: async with session.begin(): @@ -56,10 +58,6 @@ async def get_current_tenant_id() -> int | None: """ return _get_ctx_tenant_id() - -# ── Token 解析 & 验证辅助函数 ── - - def _decode_token_info(token: str) -> tuple[dict, str]: """解码 JWT token 返回 (user_info, session_id) @@ -121,10 +119,6 @@ async def _try_sliding_refresh(redis: Redis, session_id: str) -> None: expire=settings.REFRESH_TOKEN_EXPIRE_MINUTES, ) - -# ── 用户加载辅助函数 ── - - async def _load_user_from_db(db: AsyncSession, username: str): """从数据库加载用户(含角色、菜单、部门、职位全量预加载) @@ -166,10 +160,6 @@ async def _load_user_from_db(db: AsyncSession, username: str): return user - -# ── 依赖注入函数 ── - - async def get_current_user( request: Request, db: AsyncSession = Depends(db_getter), @@ -197,9 +187,10 @@ async def get_current_user( if token.startswith("Bearer"): token = token.split(" ")[1] - # 优先使用 TenantMiddleware 缓存在 scope 中的 JWT 解码结果(避免重复解码) - cached_payload = request.scope.get("_jwt_payload") - cached_user_info = request.scope.get("_jwt_user_info") + # 优先使用 TenantMiddleware 缓存在 request.state.ctx 中的 JWT 解码结果(避免重复解码) + ctx = getattr(request.state, "ctx", None) + cached_payload = ctx.jwt_payload if ctx else None + cached_user_info = ctx.jwt_user_info if ctx else None if cached_payload and cached_user_info: payload = cached_payload @@ -231,11 +222,14 @@ async def get_current_user( async with async_db_session() as lookup_db: user = await _load_user_from_db(lookup_db, username) - # 设置请求上下文 - request.scope["user_id"] = user.id - request.scope["user_username"] = user.username - request.scope["session_id"] = session_id - request.scope["session_info"] = user_info + # 设置请求上下文(仅在当前 request 对象上,业务方通过 request.state.ctx 读取) + request.state.ctx = replace( + (getattr(request.state, "ctx", None) or RequestContext()), + user_id=user.id, + user_username=user.username, + session_id=session_id, + session_info=user_info, + ) # 返回的 auth.db 指向请求级事务会话,供后续读写操作使用 auth = AuthSchema(db=db, tenant_id=tenant_id, check_data_scope=False) @@ -298,10 +292,6 @@ async def _verify_token( auth.user = user return auth - -# ── 套餐菜单缓存 ── - - async def _get_cached_tenant_menu_ids(auth: AuthSchema, tenant_id: int) -> list[int]: """获取租户可用菜单 ID,带 60s 进程级缓存 @@ -325,9 +315,6 @@ async def _get_cached_tenant_menu_ids(auth: AuthSchema, tenant_id: int) -> list[ return result -# ── 权限验证类 ── - - class AuthPermission: """权限验证类""" @@ -356,7 +343,8 @@ class AuthPermission: 返回: - AuthSchema: 认证信息对象。 """ - auth.check_data_scope = self.check_data_scope + # 用 model_copy 派生一份带正确 check_data_scope 的新实例(不修改原实例) + auth = auth.model_copy(update={"check_data_scope": self.check_data_scope}) # 超级管理员直接通过 if auth.user and auth.user.is_superuser: @@ -405,21 +393,24 @@ def require_superadmin(func): """ 装饰器:仅超级管理员可调用 Service 方法。 - 自动校验 auth 参数的 is_superuser 属性,非超管直接抛出 403。 - 适用于 classmethod Service 方法,auth 必须是第二个参数(cls 之后)。 + 自动校验 ``self.auth.user.is_superuser`` 属性,非超管直接抛出 403。 + 适用于实例方法(``Service(auth).xxx(...)``),由 ``self.auth`` 取认证上下文。 用法: - @classmethod - @require_superadmin - async def create_service(cls, auth: AuthSchema, ...) -> ...: - ... + class XxxService: + def __init__(self, auth: AuthSchema) -> None: + self.auth = auth + + @require_superadmin + async def create(self, data: ...) -> ...: + ... """ from functools import wraps @wraps(func) - async def wrapper(cls, auth: AuthSchema, *args, **kwargs): - if not auth.user or not auth.user.is_superuser: + async def wrapper(self, *args, **kwargs): + if not self.auth.user or not self.auth.user.is_superuser: raise CustomException(msg="仅平台管理员可操作") - return await func(cls, auth, *args, **kwargs) + return await func(self, *args, **kwargs) return wrapper diff --git a/backend/app/core/exceptions.py b/backend/app/core/exceptions.py index c2d9b7af..c40da545 100644 --- a/backend/app/core/exceptions.py +++ b/backend/app/core/exceptions.py @@ -14,10 +14,6 @@ from app.core.logger import logger class CustomException(Exception): - """ - 自定义异常基类 - """ - def __init__( self, msg: str = RET.EXCEPTION.msg, @@ -26,20 +22,7 @@ class CustomException(Exception): data: Any | None = None, success: bool = False, ) -> None: - """ - 初始化异常对象。 - - 参数: - - msg (str): 错误消息。 - - code (int): 业务状态码。 - - status_code (int): HTTP 状态码。 - - data (Any | None): 附加数据。 - - success (bool): 是否成功标记,默认 False。 - - 返回: - - None - """ - super().__init__(msg) # 调用父类初始化方法 + super().__init__(msg) self.status_code = status_code self.code = code self.msg = msg @@ -47,231 +30,102 @@ class CustomException(Exception): self.success = success def __str__(self) -> str: - """返回异常消息 - - 返回: - - str: 异常消息 - """ return self.msg +def _tb_source(exc: Exception) -> str: + """从 traceback 提取抛出的 文件名:行号:函数名""" + if exc.__traceback__ and (tb := traceback.extract_tb(exc.__traceback__)): + f = tb[-1] + return f"{f.filename}:{f.lineno}:{f.name}" + return "unknown" + + +_VALIDATION_ERROR_MAP: dict[str, str] = { + "Field required": "请求失败,缺少必填项!", + "value is not a valid list": "类型错误,提交参数应该为列表!", + "value is not a valid int": "类型错误,提交参数应该为整数!", + "value could not be parsed to a boolean": "类型错误,提交参数应该为布尔值!", + "Input should be a valid list": "类型错误,输入应该是一个有效的列表!", +} + + def handle_exception(app: FastAPI) -> None: - """ - 注册全局异常处理器。 - - 参数: - - app (FastAPI): 应用实例。 - - 返回: - - None - """ - @app.exception_handler(CustomException) - async def CustomExceptionHandler(request: Request, exc: CustomException) -> JSONResponse: - """自定义异常处理器 - - 参数: - request: 请求对象。 - exc: 自定义异常实例。 - - 返回: - JSONResponse: 包含错误信息的 JSON 响应。 - """ - # 从 traceback 提取异常抛出的真实位置 - source = "unknown" - if exc.__traceback__: - tb = traceback.extract_tb(exc.__traceback__) - if tb: - frame = tb[-1] - source = f"{frame.filename}:{frame.lineno}:{frame.name}" - + async def custom_exception_handler(request: Request, exc: CustomException) -> JSONResponse: logger.error( - f"[异常] {request.method} {request.url.path} | " - f"来源: {source} | " - f"错误码: {exc.code} | " - f"错误信息: {exc.msg} | " - f"详情: {exc.data}" - ) - return ErrorResponse( - msg=exc.msg, - code=exc.code, - status_code=exc.status_code, - data=exc.data, + "[自定义异常] %s %s | source=%s | code=%s | msg=%s | data=%s", + request.method, request.url.path, + _tb_source(exc), exc.code, exc.msg, exc.data, ) + return ErrorResponse(msg=exc.msg, code=exc.code, status_code=exc.status_code, data=exc.data) @app.exception_handler(HTTPException) - async def HttpExceptionHandler(request: Request, exc: HTTPException) -> JSONResponse: - """ - HTTP异常处理器 - - 参数: - - request (Request): 请求对象。 - - exc (HTTPException): HTTP异常实例。 - - 返回: - - JSONResponse: 包含错误信息的 JSON 响应。 - """ + async def http_exception_handler(request: Request, exc: HTTPException) -> JSONResponse: logger.error( - f"[HTTP异常] {request.method} {request.url.path} | 状态码: {exc.status_code} | 错误信息: {exc.detail}" + "[HTTP异常] %s %s | status_code=%s | detail=%s", + request.method, request.url.path, exc.status_code, exc.detail, ) return ErrorResponse(msg=exc.detail, status_code=exc.status_code) @app.exception_handler(RequestValidationError) - async def ValidationExceptionHandler( - request: Request, exc: RequestValidationError - ) -> JSONResponse: - """ - 请求参数验证异常处理器 - - 参数: - - request (Request): 请求对象。 - - exc (RequestValidationError): 请求参数验证异常实例。 - - 返回: - - JSONResponse: 包含错误信息的 JSON 响应。 - """ - error_mapping = { - "Field required": "请求失败,缺少必填项!", - "value is not a valid list": "类型错误,提交参数应该为列表!", - "value is not a valid int": "类型错误,提交参数应该为整数!", - "value could not be parsed to a boolean": "类型错误,提交参数应该为布尔值!", - "Input should be a valid list": "类型错误,输入应该是一个有效的列表!", - } + async def validation_exception_handler(request: Request, exc: RequestValidationError) -> JSONResponse: raw_msg = exc.errors()[0].get("msg") - msg = error_mapping.get(raw_msg, raw_msg) - # 去掉Pydantic默认的前缀“Value error”, 仅保留具体提示内容 + msg = _VALIDATION_ERROR_MAP.get(raw_msg, raw_msg) if isinstance(msg, str) and msg.startswith("Value error"): - msg = ( - msg.split(",", 1)[1].strip() - if "," in msg - else msg.replace("Value error", "").strip() - ) + msg = msg[11:].lstrip(" ,") logger.error( - f"[参数验证异常] {request.method} {request.url.path} | 错误信息: {msg} | 原始错误: {exc.errors()}" - ) - return ErrorResponse( - msg=str(msg), - status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, - data=exc.body, + "[参数验证异常] %s %s | msg=%s | errors=%s", + request.method, request.url.path, msg, exc.errors(), ) + return ErrorResponse(msg=str(msg), status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, data=exc.body) @app.exception_handler(ResponseValidationError) - async def ResponseValidationHandle( - request: Request, exc: ResponseValidationError - ) -> JSONResponse: - """ - 响应参数验证异常处理器 - - 参数: - - request (Request): 请求对象。 - - exc (ResponseValidationError): 响应参数验证异常实例。 - - 返回: - - JSONResponse: 包含错误信息的 JSON 响应。 - """ + async def response_validation_handler(request: Request, exc: ResponseValidationError) -> JSONResponse: logger.error( - f"[响应验证异常] {request.method} {request.url.path} | 错误信息: 响应数据格式错误 | 详情: {exc.errors()}" - ) - return ErrorResponse( - msg="服务器响应格式错误", - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - data=exc.body, + "[响应验证异常] %s %s | errors=%s", + request.method, request.url.path, exc.errors(), ) + return ErrorResponse(msg="服务器响应格式错误", status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, data=exc.body) @app.exception_handler(SQLAlchemyError) - async def SQLAlchemyExceptionHandler(request: Request, exc: SQLAlchemyError) -> JSONResponse: - """ - 数据库异常处理器 - - 参数: - - request (Request): 请求对象。 - - exc (SQLAlchemyError): 数据库异常实例。 - - 返回: - - JSONResponse: 包含错误信息的 JSON 响应。 - """ + async def sqlalchemy_exception_handler(request: Request, exc: SQLAlchemyError) -> JSONResponse: exc_type = type(exc).__name__ logger.error( - f"[数据库异常] {request.method} {request.url.path} | 错误类型: {exc_type} | 错误详情: {exc!s}" + "[数据库异常] %s %s | type=%s | detail=%s", + request.method, request.url.path, exc_type, exc, ) - # 完整性约束(唯一键冲突 / 外键引用 / NOT NULL 等) if isinstance(exc, IntegrityError): detail = str(exc.orig) if exc.orig else str(exc) - msg = "数据已存在或违反完整性约束" if "Duplicate entry" in detail: - msg = "数据重复,请检查唯一字段" - elif "foreign key constraint" in detail: - msg = "存在关联数据,无法删除" - elif "cannot be null" in detail: - msg = "必填字段缺失" - return ErrorResponse(msg=msg, status_code=status.HTTP_409_CONFLICT, data=detail) + return ErrorResponse(msg="数据重复,请检查唯一字段", status_code=status.HTTP_409_CONFLICT, data=detail) + if "foreign key constraint" in detail: + return ErrorResponse(msg="存在关联数据,无法删除", status_code=status.HTTP_409_CONFLICT, data=detail) + if "cannot be null" in detail: + return ErrorResponse(msg="必填字段缺失", status_code=status.HTTP_409_CONFLICT, data=detail) + return ErrorResponse(msg="数据已存在或违反完整性约束", status_code=status.HTTP_409_CONFLICT, data=detail) - # 数据库连接类异常 - if "connect" in str(exc).lower() or "connection" in str(exc).lower(): - return ErrorResponse( - msg="数据库连接失败", - status_code=status.HTTP_503_SERVICE_UNAVAILABLE, - data=exc_type, - ) - - return ErrorResponse( - msg=f"数据库操作失败: {exc_type}", - status_code=status.HTTP_400_BAD_REQUEST, - data=str(exc), - ) + lower = str(exc).lower() + if "connect" in lower or "connection" in lower: + return ErrorResponse(msg="数据库连接失败", status_code=status.HTTP_503_SERVICE_UNAVAILABLE, data=exc_type) + return ErrorResponse(msg=f"数据库操作失败: {exc_type}", status_code=status.HTTP_400_BAD_REQUEST, data=str(exc)) @app.exception_handler(ValueError) - async def ValueExceptionHandler(request: Request, exc: ValueError) -> JSONResponse: - """ - 值异常处理器 - - 参数: - - request (Request): 请求对象。 - - exc (ValueError): 值异常实例。 - - 返回: - - JSONResponse: 包含错误信息的 JSON 响应。 - """ - logger.error(f"[值异常] {request.method} {request.url.path} | 错误信息: {exc!s}") + async def value_exception_handler(request: Request, exc: ValueError) -> JSONResponse: + logger.error("[值异常] %s %s | msg=%s", request.method, request.url.path, exc) return ErrorResponse(msg=str(exc), status_code=status.HTTP_400_BAD_REQUEST) @app.exception_handler(FieldValidationError) - async def FieldValidationExceptionHandler( - request: Request, exc: FieldValidationError - ) -> JSONResponse: - """ - 字段验证异常处理器 - - 参数: - - request (Request): 请求对象。 - - exc (FieldValidationError): 字段验证异常实例。 - - 返回: - - JSONResponse: 包含错误信息的 JSON 响应。 - """ - logger.error(f"[字段验证异常] {request.method} {request.url.path} | 错误信息: {exc.message}") + async def field_validation_exception_handler(request: Request, exc: FieldValidationError) -> JSONResponse: + logger.error("[字段验证异常] %s %s | msg=%s", request.method, request.url.path, exc.message) return ErrorResponse(msg=exc.message, status_code=status.HTTP_422_UNPROCESSABLE_ENTITY) @app.exception_handler(Exception) - async def AllExceptionHandler(request: Request, exc: Exception) -> JSONResponse: - """ - 全局异常处理器 - - 参数: - - request (Request): 请求对象。 - - exc (Exception): 异常实例。 - - 返回: - - JSONResponse: 包含错误信息的 JSON 响应。 - """ + async def all_exception_handler(request: Request, exc: Exception) -> JSONResponse: exc_type = type(exc).__name__ logger.error( - f"[未捕获异常] {request.method} {request.url.path} | 错误类型: {exc_type} | 错误详情: {exc!s}" - ) - # 对于未捕获的异常,返回通用错误信息 - return ErrorResponse( - msg="服务器内部错误", - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - data=None, + "[未捕获异常] %s %s | type=%s | detail=%s", + request.method, request.url.path, exc_type, exc, ) + return ErrorResponse(msg="服务器内部错误", status_code=status.HTTP_500_INTERNAL_SERVER_ERROR) diff --git a/backend/app/core/logger.py b/backend/app/core/logger.py index 54159286..af35f82a 100644 --- a/backend/app/core/logger.py +++ b/backend/app/core/logger.py @@ -1,130 +1,49 @@ import logging import sys -from contextvars import ContextVar, Token -from typing import Any from loguru import logger from app.config.path_conf import LOG_DIR from app.config.setting import settings - - -class RequestContext: - """请求级别上下文管理器 - 为每个请求维护独立的上下文信息(correlation_id、user_id 等), - 通过 Python contextvars 实现异步安全传递。 - 典型用法: - ctx = RequestContext() - ctx.set_correlation_id("abc-123") - cid = RequestContext.get_correlation_id() - """ - - _correlation_id: ContextVar[str] = ContextVar("correlation_id", default="") - _user_id: ContextVar[int | None] = ContextVar("user_id", default=None) - _tenant_id: ContextVar[int | None] = ContextVar("tenant_id", default=None) - _request_path: ContextVar[str] = ContextVar("request_path", default="") - - @classmethod - def set_correlation_id(cls, correlation_id: str) -> Token: - return cls._correlation_id.set(correlation_id) - - @classmethod - def get_correlation_id(cls) -> str: - return cls._correlation_id.get() - - @classmethod - def set_user_id(cls, user_id: int | None) -> Token: - return cls._user_id.set(user_id) - - @classmethod - def get_user_id(cls) -> int | None: - return cls._user_id.get() - - @classmethod - def set_tenant_id(cls, tenant_id: int | None) -> Token: - return cls._tenant_id.set(tenant_id) - - @classmethod - def get_tenant_id(cls) -> int | None: - return cls._tenant_id.get() - - @classmethod - def set_request_path(cls, path: str) -> Token: - return cls._request_path.set(path) - - @classmethod - def get_request_path(cls) -> str: - return cls._request_path.get() - - @classmethod - def get_extra_fields(cls) -> dict[str, Any]: - fields: dict[str, Any] = { - "correlation_id": cls.get_correlation_id(), - } - user_id = cls.get_user_id() - if user_id is not None: - fields["user_id"] = user_id - tenant_id = cls.get_tenant_id() - if tenant_id is not None: - fields["tenant_id"] = tenant_id - return fields +from app.core.request_context import get_correlation_id def _context_patcher(record: dict) -> None: - """在日志格式化前,将请求上下文注入 record["extra"]。 - - 作为 logger.configure(patcher=...) 的回调,每次日志输出前被调用。 - 消息可能包含 { } (如 dict repr),因此拼接到 record["extra"] 中, - 由 loguru 静态格式串的 {extra[...]} 渲染,避开二次 format_map 解析。 - """ - cid = RequestContext.get_correlation_id() - uid = RequestContext.get_user_id() - parts: list[str] = [] - if cid: - parts.append(f"cid={cid[:8]}") - if uid is not None: - parts.append(f"uid={uid}") - record["extra"]["ctx"] = " | " + " ".join(parts) if parts else "" + cid = get_correlation_id() + record["extra"]["ctx"] = f" | cid={cid[:8]}" if cid else "" class InterceptHandler(logging.Handler): - """日志拦截处理器:将所有 Python 标准日志重定向到 Loguru""" + """将标准库 logging 重定向到 Loguru""" def emit(self, record: logging.LogRecord) -> None: try: level = logger.level(record.levelname).name except ValueError: level = record.levelno - frame, depth = logging.currentframe(), 2 while frame and frame.f_code.co_filename == logging.__file__: frame = frame.f_back depth += 1 - logger.opt(depth=depth, exception=record.exc_info).log(level, "{}", record.getMessage()) def setup_logger() -> None: LOG_DIR.mkdir(parents=True, exist_ok=True) - logger.remove() - - # 注入 patcher:每条日志格式化前拉取 RequestContext logger.configure(patcher=_context_patcher) - # 静态格式串:loguru 用自己的模板引擎渲染,不会对消息内容做二次 format_map - LOG_FORMAT = ( + LOG_FMT = ( "{time:YYYY-MM-DD HH:mm:ss.SSS} | " "{level: <8} | " "{name}:{function}:{line} - " "{message}" "{extra[ctx]}" ) - logger.add(sys.stdout, format=LOG_FORMAT, level=settings.LOGGER_LEVEL) - + logger.add(sys.stdout, format=LOG_FMT, level=settings.LOGGER_LEVEL) logger.add( str(LOG_DIR / "fastapiadmin.log"), - format=LOG_FORMAT, + format=LOG_FMT, level="INFO", rotation="00:00", retention=30, @@ -133,14 +52,10 @@ def setup_logger() -> None: ) logging.basicConfig(handlers=[InterceptHandler()], level=settings.LOGGER_LEVEL, force=True) - - logger_name_list = [k for k in logging.root.manager.loggerDict if isinstance(k, str)] - logger_name_list.extend(["uvicorn", "uvicorn.error", "uvicorn.access"]) - - for logger_name in logger_name_list: - std_logger = logging.getLogger(logger_name) - std_logger.handlers = [InterceptHandler()] - std_logger.propagate = False + for name in [k for k in logging.root.manager.loggerDict if isinstance(k, str)] + ["uvicorn", "uvicorn.error", "uvicorn.access"]: + std = logging.getLogger(name) + std.handlers = [InterceptHandler()] + std.propagate = False setup_logger() diff --git a/backend/app/core/middlewares.py b/backend/app/core/middlewares.py index 81ff7ba5..484c50ef 100644 --- a/backend/app/core/middlewares.py +++ b/backend/app/core/middlewares.py @@ -1,11 +1,9 @@ import json import time import uuid +from dataclasses import replace -from starlette.middleware.base import ( - BaseHTTPMiddleware, - RequestResponseEndpoint, -) +from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint from starlette.middleware.cors import CORSMiddleware from starlette.middleware.gzip import GZipMiddleware from starlette.requests import Request @@ -16,14 +14,24 @@ from app.api.v1.module_system.params.service import ParamsService from app.common.response import ErrorResponse from app.config.setting import settings from app.core.exceptions import CustomException -from app.core.logger import RequestContext, logger +from app.core.logger import logger +from app.core.request_context import RequestContext, clear_current_tenant, reset_correlation_id, set_correlation_id, set_current_tenant from app.core.security import decode_access_token -from app.core.tenant import clear_current_tenant, set_current_tenant + + +def _strip_bearer(authorization: str) -> str | None: + """从 Authorization header 提取 token,非 Bearer 返回 None。""" + v = authorization.strip() + if v.lower().startswith("bearer "): + v = v[7:].strip() + elif v.lower().startswith("bearer"): + v = v[6:].strip() + else: + return None + return v or None class CustomCORSMiddleware(CORSMiddleware): - """CORS跨域中间件""" - def __init__(self, app: ASGIApp) -> None: super().__init__( app, @@ -36,326 +44,165 @@ class CustomCORSMiddleware(CORSMiddleware): class RequestLogMiddleware(BaseHTTPMiddleware): - """ - 记录请求日志中间件: 提供一个基础的中间件类,允许你自定义请求和响应处理逻辑。 - """ + """请求日志 & 演示模式拦截""" def __init__(self, app: ASGIApp) -> None: super().__init__(app) @staticmethod - def _extract_session_id_from_request(request: Request) -> str | None: - """ - 从请求中提取session_id(支持从Token或已设置的scope中获取) + def _hydrate_session_id(request: Request) -> None: + """从 request.state.ctx 或 JWT 中提取 session_id 并写入 ctx(纯 side-effect)。""" + ctx = getattr(request.state, "ctx", None) + if ctx: + if ctx.session_id: + return + if ctx.jwt_user_info: + sid = ctx.jwt_user_info.get("session_id") + if sid: + request.state.ctx = replace(ctx, session_id=sid) + return - 参数: - - request (Request): 请求对象 - - 返回: - - str | None: 会话ID,如果无法提取则返回None - """ - # 1. 先检查 scope 中是否已经有 session_id(登录接口会设置) - session_id = request.scope.get("session_id") - if session_id: - return session_id - - # 2. 检查 TenantMiddleware 缓存到 scope 的 JWT user_info(避免重复解码) - user_info = request.scope.get("_jwt_user_info") - if user_info: - session_id = user_info.get("session_id") - if session_id: - request.scope["session_id"] = session_id - return session_id - - # 3. 尝试从 Authorization Header 中提取 + token = _strip_bearer(request.headers.get("Authorization", "")) + if not token: + return try: - authorization = request.headers.get("Authorization") - if not authorization: - return None - - # 处理Bearer token (大小写不敏感) - token = authorization - if authorization.lower().startswith("bearer "): - token = authorization[7:].strip() - elif authorization.lower().startswith("bearer"): - token = authorization[6:].strip() - - # 解码token payload = decode_access_token(token) if not payload or not hasattr(payload, "sub"): - return None - - # 从payload中提取session_id + return user_info = json.loads(payload.sub) - session_id = user_info.get("session_id") - - # 同时设置到request.scope中,避免后续重复解析 - if session_id: - request.scope["session_id"] = session_id - - return session_id + sid = user_info.get("session_id") + if sid: + base = ctx or RequestContext() + request.state.ctx = replace(base, session_id=sid) except Exception: - # 解析失败静默处理,返回None(可能是未认证请求) - return None + pass async def dispatch(self, request: Request, call_next: RequestResponseEndpoint) -> Response: - """ - 记录请求日志并透传响应。 - - 参数: - - request (Request): 当前请求。 - - call_next (RequestResponseEndpoint): 下一层 ASGI 可调用对象。 - - 返回: - - Response: 下游中间件/路由产生的响应。 - """ start_time = time.time() + self._hydrate_session_id(request) - # 尝试提取session_id - session_id = self._extract_session_id_from_request(request) - - # 组装请求日志字段 - log_fields = ( - f"请求来源: {request.client.host if request.client else '未知'}," - f"请求方法: {request.method}," - f"请求路径: {request.url.path}" - ) - logger.info(log_fields) + logger.info("请求: %s %s | client=%s", request.method, request.url.path, + request.client.host if request.client else "unknown") try: - # 初始化响应变量 - response = None - - # 获取请求路径 path = request.scope.get("path") - - # 尝试获取客户端真实IP - request_ip = None request_ip = ( - x_forwarded_for.split(",")[0].strip() + (x_forwarded_for.split(",")[0].strip()) if (x_forwarded_for := request.headers.get("X-Forwarded-For")) - else request.client.host - if request.client - else None + else request.client.host if request.client else None ) - # 检查是否启用演示模式 - demo_enable = False - ip_white_list = [] - white_api_list_path = [] - ip_black_list = [] try: - # 从应用实例获取Redis连接 redis = request.app.state.redis - if not redis: - raise CustomException(msg="无法获取Redis连接") + config = await ParamsService.get_system_config_for_middleware(redis) + demo_enable = config["demo_enable"] + ip_white_list = config["ip_white_list"] + white_api_list_path = config["white_api_list_path"] + ip_black_list = config["ip_black_list"] + except Exception: + demo_enable = False + ip_white_list, white_api_list_path, ip_black_list = [], [], [] - # 使用ParamsService获取系统配置 - system_config = await ParamsService.get_system_config_for_middleware(redis) - # 提取配置值 - demo_enable = system_config["demo_enable"] - ip_white_list = system_config["ip_white_list"] - white_api_list_path = system_config["white_api_list_path"] - ip_black_list = system_config["ip_black_list"] - - except Exception as e: - logger.error(f"获取系统配置失败: {e}") - - # 检查是否需要拦截请求 - should_block = False - block_reason = "" - - # 1. 首先检查IP是否在黑名单中 - if request_ip and request_ip in ip_black_list: - should_block = True - block_reason = f"IP地址 {request_ip} 在黑名单中" - - # 2. 如果不在黑名单中,检查是否在演示模式下需要拦截 - elif demo_enable in (True, "true", "True") and request.method != "GET": - # 在演示模式下,非GET请求需要检查白名单 - is_ip_whitelisted = request_ip in ip_white_list - is_path_whitelisted = path in white_api_list_path - - if not is_ip_whitelisted and not is_path_whitelisted: - should_block = True - block_reason = f"演示模式下拦截非GET请求,IP: {request_ip}, 路径: {path}" + should_block = (request_ip and request_ip in ip_black_list) or ( + demo_enable and request.method != "GET" + and request_ip not in ip_white_list + and path not in white_api_list_path + ) if should_block: - # 增强安全审计:记录详细的拦截日志 - logger.warning( - " | ".join([ - f"会话ID: {session_id or '未认证'}", - f"请求被拦截: {block_reason}", - f"请求来源: {request_ip}", - f"请求方法: {request.method}", - f"请求路径: {path}", - f"用户代理: {request.headers.get('user-agent', '未知')}", - f"演示模式: {demo_enable}", - ]) - ) - # 拦截请求 + logger.warning("演示模式拦截: %s %s | ip=%s", request.method, path, request_ip) return ErrorResponse(msg="演示环境,禁止操作") - # 正常处理请求 - response = await call_next(request) - # 计算处理时间并添加到响应头 + response = await call_next(request) process_time = round(time.time() - start_time, 5) response.headers["X-Process-Time"] = str(process_time) - - # 构建响应日志信息 - response_info = f"响应状态: {response.status_code}, 处理时间: {round(process_time * 1000, 3)}ms" - logger.info(response_info) - + logger.info("响应: %s | %.1fms", response.status_code, process_time * 1000) return response - except CustomException as e: - logger.exception(f"中间件处理异常: {e!s}") + logger.exception(f"中间件异常: {e!s}") return ErrorResponse(msg="系统异常,请联系管理员", data=str(e)) class CustomGZipMiddleware(GZipMiddleware): - """GZip压缩中间件""" - def __init__(self, app: ASGIApp) -> None: - super().__init__( - app, - minimum_size=settings.GZIP_MIN_SIZE, - compresslevel=settings.GZIP_COMPRESS_LEVEL, - ) + super().__init__(app, minimum_size=settings.GZIP_MIN_SIZE, compresslevel=settings.GZIP_COMPRESS_LEVEL) class SecurityHeadersMiddleware(BaseHTTPMiddleware): - """安全响应头中间件""" - def __init__(self, app: ASGIApp) -> None: super().__init__(app) - from app.config.setting import settings - self._hsts = settings.HSTS_ENABLE async def dispatch(self, request: Request, call_next: RequestResponseEndpoint) -> Response: response = await call_next(request) - if self._hsts: - response.headers["Strict-Transport-Security"] = ( - "max-age=31536000; includeSubDomains; preload" - ) - + response.headers["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains; preload" response.headers["X-Content-Type-Options"] = "nosniff" response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin" - response.headers["Permissions-Policy"] = ( - "camera=(), microphone=(), geolocation=(), " - "payment=(), usb=(), magnetometer=(), " - "gyroscope=()" - ) - + response.headers["Permissions-Policy"] = "camera=(), microphone=(), geolocation=(), payment=(), usb=(), magnetometer=(), gyroscope=()" return response class CorrelationIdMiddleware(BaseHTTPMiddleware): - """请求关联 ID 中间件""" - def __init__(self, app: ASGIApp) -> None: - self.CORRELATION_ID_HEADER = "X-Correlation-ID" + self._header = "X-Correlation-ID" super().__init__(app) async def dispatch(self, request: Request, call_next: RequestResponseEndpoint) -> Response: - correlation_id = request.headers.get(self.CORRELATION_ID_HEADER, "") - if not correlation_id: - correlation_id = str(uuid.uuid4()) - - token = RequestContext.set_correlation_id(correlation_id) - RequestContext.set_request_path(request.url.path) - + cid = request.headers.get(self._header) or str(uuid.uuid4()) + token = set_correlation_id(cid) try: response = await call_next(request) - response.headers[self.CORRELATION_ID_HEADER] = correlation_id + response.headers[self._header] = cid return response finally: - RequestContext._correlation_id.reset(token) + reset_correlation_id(token) -# ── 租户白名单 ── - -_TENANT_WHITELIST_PREFIXES: tuple[str, ...] = ( - "/docs", "/redoc", "/ljdoc", "/openapi.json", "/metrics", "/static", +_TENANT_WHITELIST_PREFIXES = ("/docs", "/redoc", "/ljdoc", "/openapi.json", "/metrics", "/static") +_TENANT_WHITELIST_PATHS = ( + "/api/v1/system/auth/login", "/api/v1/system/auth/captcha", + "/api/v1/system/auth/refresh", "/api/v1/health", "/api/v1/common/health", ) -_TENANT_WHITELIST_PATHS: tuple[str, ...] = ( - "/api/v1/system/auth/login", - "/api/v1/system/auth/captcha", - "/api/v1/system/auth/refresh", - "/api/v1/health", - "/api/v1/common/health", -) +_WHITELIST_ALL = _TENANT_WHITELIST_PATHS + tuple(settings.TENANT_WHITELIST_PATHS) def _tenant_is_whitelisted(path: str) -> bool: - for wp in _TENANT_WHITELIST_PATHS: - if path.startswith(wp): - return True - for prefix in _TENANT_WHITELIST_PREFIXES: - if path.startswith(prefix): - return True - for exclude_path in settings.TENANT_WHITELIST_PATHS: - if path.startswith(exclude_path): - return True - return False + return any(path.startswith(p) for p in _WHITELIST_ALL) or \ + any(path.startswith(p) for p in _TENANT_WHITELIST_PREFIXES) -def _extract_tenant_from_token(request: Request) -> tuple[int | None, bool]: - authorization = request.headers.get("Authorization") - if not authorization: - return None, False - token = authorization - if authorization.lower().startswith("bearer "): - token = authorization[7:] - elif authorization.lower().startswith("bearer"): - token = authorization[6:] - if not token.strip(): - return None, False +def _extract_tenant_from_token(request: Request) -> int | None: + token = _strip_bearer(request.headers.get("Authorization", "")) + if not token: + return None try: payload = decode_access_token(token) if not payload or not hasattr(payload, "sub"): - return None, False + return None user_info = json.loads(payload.sub) - # 缓存到 request.scope,避免后续中间件/依赖重复解码 JWT - request.scope["_jwt_payload"] = payload - request.scope["_jwt_user_info"] = user_info - tenant_id = user_info.get("tenant_id") - is_super_admin = user_info.get("is_super_admin", False) - return tenant_id, is_super_admin + base = getattr(request.state, "ctx", None) or RequestContext() + request.state.ctx = replace(base, jwt_payload=payload, jwt_user_info=user_info) + return user_info.get("tenant_id") except Exception: - return None, False + return None class TenantMiddleware(BaseHTTPMiddleware): - """租户上下文中间件""" - def __init__(self, app: ASGIApp) -> None: super().__init__(app) async def dispatch(self, request: Request, call_next: RequestResponseEndpoint) -> Response: path = request.url.path - - if request.method == "OPTIONS": + if request.method == "OPTIONS" or _tenant_is_whitelisted(path): return await call_next(request) - - if _tenant_is_whitelisted(path): - return await call_next(request) - try: - tenant_id, is_super_admin = _extract_tenant_from_token(request) - set_current_tenant(tenant_id, is_super_admin) - if tenant_id is not None: - logger.debug( - "租户上下文已设置: tenant_id={}, is_super_admin={}, path={}", - tenant_id, is_super_admin, path, - ) + tenant_id = _extract_tenant_from_token(request) + set_current_tenant(tenant_id) except Exception: - logger.exception("租户中间件处理异常: path={}", path) + logger.exception("租户中间件异常: path=%s", path) try: - response = await call_next(request) - return response + return await call_next(request) finally: clear_current_tenant() diff --git a/backend/app/core/payment/__init__.py b/backend/app/core/payment/__init__.py deleted file mode 100644 index 1ad23b1c..00000000 --- a/backend/app/core/payment/__init__.py +++ /dev/null @@ -1,58 +0,0 @@ -"""支付网关工厂""" -from __future__ import annotations - -from app.config.setting import settings -from app.core.logger import logger - -from .alipay import AlipayGateway -from .base import BasePaymentGateway, CallbackResult, PaymentInfo -from .mock import MockPaymentGateway - - -def create_payment_gateway(method: str = "") -> BasePaymentGateway: - """创建支付网关实例 - - 参数: - method: "alipay" / "wxpay" / ""(根据配置自动选择) - - 优先级: - 1. 指定渠道 - 2. ALIPAY 配置就绪 → AlipayGateway - 3. 兜底 → MockPaymentGateway - """ - if method == "alipay": - logger.info("🔔 使用支付宝支付网关") - return AlipayGateway() - - if method == "wxpay": - logger.info("🔔 使用微信支付网关(未实现,回退 Mock)") - return MockPaymentGateway() - - # 自动检测:如果配置了支付宝密钥则使用真实网关,否则 Mock - if settings.PAYMENT_ALIPAY_APP_ID and settings.PAYMENT_ALIPAY_PRIVATE_KEY: - logger.info("🔔 自动选择支付宝支付网关") - return AlipayGateway() - - logger.info("🔔 使用 Mock 支付网关(开发模式)") - return MockPaymentGateway() - - -# 获取当前 Mock 网关(用于开发环境手动触发回调) -_mock_gateway: MockPaymentGateway | None = None - - -def get_mock_gateway() -> MockPaymentGateway: - """获取全局 Mock 网关实例(用于开发环境)""" - global _mock_gateway - if _mock_gateway is None: - _mock_gateway = MockPaymentGateway() - return _mock_gateway - - -__all__ = [ - "BasePaymentGateway", - "PaymentInfo", - "CallbackResult", - "create_payment_gateway", - "get_mock_gateway", -] diff --git a/backend/app/core/payment/alipay.py b/backend/app/core/payment/alipay.py deleted file mode 100644 index bc820922..00000000 --- a/backend/app/core/payment/alipay.py +++ /dev/null @@ -1,120 +0,0 @@ -"""支付宝支付网关 - -依赖 cryptography(已在 pyproject.toml 中)进行 RSA 签名验签。 -""" -from __future__ import annotations - -import json -from datetime import datetime -from typing import Any -from urllib.parse import urlencode - -from cryptography.hazmat.primitives import hashes, serialization -from cryptography.hazmat.primitives.asymmetric import padding, rsa - -from app.config.setting import settings - -from .base import BasePaymentGateway, CallbackResult, PaymentInfo - - -class AlipayGateway(BasePaymentGateway): - """支付宝支付网关(App 支付 / 网站支付 / Native 扫码)""" - - GATEWAY_URL = "https://openapi.alipay.com/gateway.do" - DEV_GATEWAY_URL = "https://openapi-sandbox.dl.alipaydev.com/gateway.do" - - def __init__(self) -> None: - self.app_id = settings.PAYMENT_ALIPAY_APP_ID or "" - self._private_key = (settings.PAYMENT_ALIPAY_PRIVATE_KEY or "").encode() - self._alipay_public_key = (settings.PAYMENT_ALIPAY_PUBLIC_KEY or "").encode() - self.is_sandbox = settings.PAYMENT_ALIPAY_SANDBOX - self._gateway = self.DEV_GATEWAY_URL if self.is_sandbox else self.GATEWAY_URL - - def _sign(self, params: dict[str, Any]) -> str: - """RSA2 签名""" - sorted_params = sorted((k, v) for k, v in params.items() if v != "" and v is not None) - sign_str = "&".join(f"{k}={v}" for k, v in sorted_params) - - private_key_obj = serialization.load_pem_private_key( - self._private_key, - password=None, - ) - if not isinstance(private_key_obj, rsa.RSAPrivateKey): - raise TypeError("私钥类型不是 RSA") - - signature = private_key_obj.sign( - sign_str.encode("utf-8"), - padding.PKCS1v15(), - hashes.SHA256(), - ) - import base64 - return base64.b64encode(signature).decode("utf-8") - - def _verify(self, params: dict[str, Any], signature: str) -> bool: - """验证 RSA2 签名""" - sorted_params = sorted( - (k, v) for k, v in params.items() - if k != "sign" and k != "sign_type" and v != "" and v is not None - ) - sign_str = "&".join(f"{k}={v}" for k, v in sorted_params) - - try: - public_key_obj = serialization.load_pem_public_key(self._alipay_public_key) - if not isinstance(public_key_obj, rsa.RSAPublicKey): - return False - - import base64 - public_key_obj.verify( - base64.b64decode(signature), - sign_str.encode("utf-8"), - padding.PKCS1v15(), - hashes.SHA256(), - ) - return True - except Exception: - return False - - async def create_payment( - self, order_no: str, amount: int, subject: str, notify_url: str - ) -> PaymentInfo: - """创建支付宝支付(返回 H5 支付页面 URL)""" - biz_content = json.dumps({ - "out_trade_no": order_no, - "total_amount": f"{amount / 100:.2f}", - "subject": subject, - "product_code": "FAST_INSTANT_TRADE_PAY", - }, ensure_ascii=False) - - params = { - "app_id": self.app_id, - "method": "alipay.trade.page.pay", - "format": "JSON", - "charset": "utf-8", - "sign_type": "RSA2", - "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), - "version": "1.0", - "notify_url": notify_url, - "biz_content": biz_content, - } - params["sign"] = self._sign(params) - - pay_url = f"{self._gateway}?{urlencode(params)}" - return PaymentInfo(pay_url=pay_url, trade_no="", raw=params) - - async def verify_callback(self, data: dict[str, Any]) -> CallbackResult: - """验证支付宝异步通知回调""" - sign = data.pop("sign", "") - _ = data.pop("sign_type", "") # noqa - - verified = self._verify(data, sign) if sign else False - - if verified and data.get("trade_status") in ("TRADE_SUCCESS", "TRADE_FINISHED"): - return CallbackResult( - verified=True, - transaction_id=data.get("trade_no"), - order_id=None, # 由调用方从 out_trade_no 解析 - amount=int(float(data.get("total_amount", 0)) * 100), - raw=data, - ) - - return CallbackResult(verified=False, raw=data) diff --git a/backend/app/core/payment/base.py b/backend/app/core/payment/base.py deleted file mode 100644 index 29905f25..00000000 --- a/backend/app/core/payment/base.py +++ /dev/null @@ -1,59 +0,0 @@ -"""支付网关抽象基类""" -from __future__ import annotations - -from abc import ABC, abstractmethod -from dataclasses import dataclass, field -from typing import Any - - -@dataclass -class PaymentInfo: - """支付信息 — 创建支付后返回""" - pay_url: str | None = None # H5 跳转 URL - qr_code_url: str | None = None # Native 二维码 URL - trade_no: str = "" # 网关交易号 - raw: dict[str, Any] = field(default_factory=dict) # 原始响应 - - -@dataclass -class CallbackResult: - """回调验签结果""" - verified: bool = False # 验签是否通过 - transaction_id: str | None = None # 网关交易号 - order_id: int | None = None # 本系统订单 ID - amount: int = 0 # 支付金额(分) - raw: dict[str, Any] = field(default_factory=dict) # 原始回调数据 - - -class BasePaymentGateway(ABC): - """支付网关抽象基类 - - 所有支付渠道(Alipay、WxPay、Mock)需实现此接口。 - """ - - @abstractmethod - async def create_payment(self, order_no: str, amount: int, subject: str, notify_url: str) -> PaymentInfo: - """创建支付 - - 参数: - order_no: 本系统订单号 - amount: 金额(分) - subject: 商品描述 - notify_url: 异步通知回调 URL - - 返回: - PaymentInfo: 支付信息(支付 URL / 二维码等) - """ - ... - - @abstractmethod - async def verify_callback(self, data: dict[str, Any]) -> CallbackResult: - """验证异步通知回调签名 - - 参数: - data: 回调原始数据 - - 返回: - CallbackResult: 验签结果 - """ - ... diff --git a/backend/app/core/payment/mock.py b/backend/app/core/payment/mock.py deleted file mode 100644 index 780df448..00000000 --- a/backend/app/core/payment/mock.py +++ /dev/null @@ -1,68 +0,0 @@ -"""Mock 支付网关 — 开发/测试用,模拟支付成功""" -from __future__ import annotations - -import uuid -from typing import Any - -from .base import BasePaymentGateway, CallbackResult, PaymentInfo - - -class MockPaymentGateway(BasePaymentGateway): - """Mock 支付网关 - - 开发模式下使用,无需真实支付渠道配置即可测试完整支付流程。 - - create_payment: 返回模拟的 QR 码/支付 URL - - verify_callback: 标记为验签通过 - """ - - def __init__(self) -> None: - self._pending_orders: dict[str, dict] = {} - - async def create_payment( - self, order_no: str, amount: int, subject: str, notify_url: str - ) -> PaymentInfo: - trade_no = f"MOCK{uuid.uuid4().hex[:16].upper()}" - self._pending_orders[order_no] = { - "trade_no": trade_no, - "amount": amount, - "subject": subject, - } - - # 模拟支付页面(开发环境可用) - pay_url = f"/mock-pay?order_no={order_no}&amount={amount}" - qr_url = f"https://api.qrserver.com/v1/create-qr-code/?size=200x200&data={pay_url}" - - return PaymentInfo( - pay_url=pay_url, - qr_code_url=qr_url, - trade_no=trade_no, - raw={"order_no": order_no, "amount": amount, "is_mock": True}, - ) - - async def verify_callback(self, data: dict[str, Any]) -> CallbackResult: - order_no = data.get("order_no", "") - pending = self._pending_orders.pop(order_no, None) - - if pending: - return CallbackResult( - verified=True, - transaction_id=pending["trade_no"], - order_id=data.get("order_id"), - amount=pending["amount"], - raw={**data, **pending}, - ) - return CallbackResult( - verified=False, - raw=data, - ) - - def get_mock_callback_data(self, order_id: int, order_no: str) -> dict: - """生成模拟回调数据,供开发环境手动触发""" - pending = self._pending_orders.get(order_no, {}) - return { - "order_id": order_id, - "order_no": order_no, - "trade_no": pending.get("trade_no", f"MOCK{uuid.uuid4().hex[:16].upper()}"), - "total_amount": str(pending.get("amount", 0) / 100), - "trade_status": "TRADE_SUCCESS", - } diff --git a/backend/app/core/permission.py b/backend/app/core/permission.py index a4696c39..83af71f6 100644 --- a/backend/app/core/permission.py +++ b/backend/app/core/permission.py @@ -73,20 +73,20 @@ class Permission: ) # 根据策略选择过滤方法 - if strategy == PermissionFilterStrategy.ROLE_BASED: - return await self.__filter_by_role_based() - elif strategy == PermissionFilterStrategy.DEPT_BASED: - return await self.__filter_by_dept_based() - elif strategy == PermissionFilterStrategy.SELF_ONLY: - return await self.__filter_by_self_only() - elif strategy == PermissionFilterStrategy.USER_ROLE: - return await self.__filter_by_user_role() + if strategy == PermissionFilterStrategy.MENU_AUTH: + return await self.__filter_by_menu_auth() + elif strategy == PermissionFilterStrategy.DEPT_RELATION: + return await self.__filter_by_dept_relation() + elif strategy == PermissionFilterStrategy.OWN: + return await self.__filter_by_own() + elif strategy == PermissionFilterStrategy.USER_BINDING: + return await self.__filter_by_user_binding() else: return await self.__filter_by_data_scope() - async def __filter_by_role_based(self) -> ColumnElement | None: + async def __filter_by_menu_auth(self) -> ColumnElement | None: """ - 基于角色授权的权限过滤(适用于菜单等) + 基于角色-菜单授权的过滤(适用于菜单模型) 只显示用户角色授权的菜单,同时受租户套餐约束。 """ @@ -124,9 +124,9 @@ class Permission: return id_attr == -1 return None - async def __filter_by_user_role(self) -> ColumnElement | None: + async def __filter_by_user_binding(self) -> ColumnElement | None: """ - 基于当前用户绑定角色的权限过滤(适用于角色列表) + 基于当前用户绑定角色的过滤(适用于角色模型) 只显示当前用户绑定的角色 """ @@ -143,9 +143,9 @@ class Permission: return id_attr.in_(role_ids) return None - async def __filter_by_dept_based(self) -> ColumnElement | None: + async def __filter_by_dept_relation(self) -> ColumnElement | None: """ - 基于部门关联的权限过滤(适用于部门、角色等) + 基于部门关联的过滤(适用于部门模型、角色模型) 根据用户的部门权限范围过滤数据 """ @@ -183,9 +183,9 @@ class Permission: else: return None - async def __filter_by_self_only(self) -> ColumnElement | None: + async def __filter_by_own(self) -> ColumnElement | None: """ - 仅本人数据权限过滤 + 仅本人数据过滤 """ created_id_attr = getattr(self.model, "created_id", None) if created_id_attr is not None and self.auth.user: diff --git a/backend/app/core/request_context.py b/backend/app/core/request_context.py new file mode 100644 index 00000000..50fb29fa --- /dev/null +++ b/backend/app/core/request_context.py @@ -0,0 +1,47 @@ +"""请求级上下文""" + +from __future__ import annotations + +from contextvars import ContextVar, Token +from dataclasses import dataclass +from typing import Any + +# ── 日志注入 ── +_correlation_id: ContextVar[str] = ContextVar("correlation_id", default="") + + +def set_correlation_id(cid: str) -> Token: + return _correlation_id.set(cid) + +def get_correlation_id() -> str: + return _correlation_id.get() + +def reset_correlation_id(token: Token) -> None: + _correlation_id.reset(token) + + +# ── 租户上下文 ── +current_tenant_id: ContextVar[int | None] = ContextVar("current_tenant_id", default=None) + + +def set_current_tenant(tenant_id: int | None) -> None: + current_tenant_id.set(tenant_id) + +def get_current_tenant_id() -> int | None: + return current_tenant_id.get() + +def clear_current_tenant() -> None: + current_tenant_id.set(None) + + +# ── request.state.ctx ── + +@dataclass +class RequestContext: + jwt_payload: Any = None + jwt_user_info: dict[str, Any] | None = None + session_id: str | None = None + user_id: int | None = None + user_username: str | None = None + session_info: dict[str, Any] | None = None + login_location: str | None = None diff --git a/backend/app/core/router_class.py b/backend/app/core/router_class.py index 68db1e71..a8b98727 100644 --- a/backend/app/core/router_class.py +++ b/backend/app/core/router_class.py @@ -1,4 +1,3 @@ -import asyncio import json import time from collections.abc import Callable, Coroutine @@ -6,6 +5,7 @@ from typing import Any from fastapi import Request, Response from fastapi.routing import APIRoute +from starlette.background import BackgroundTask from app.config.setting import settings from app.core.base_schema import AuthSchema @@ -14,82 +14,37 @@ from app.core.logger import logger async def _write_operation_log_async(log_data: dict) -> None: - """ - 后台异步写入操作日志,不阻塞请求响应。 - - 在独立的 DB session 中写入,避免复用请求上下文中的 session。 - """ from app.api.v1.module_system.log.schema import OperationLogCreateSchema from app.api.v1.module_system.log.service import OperationLogService try: async with async_db_session() as _session: async with _session.begin(): _auth = AuthSchema(db=_session) - await OperationLogService.create_service( - data=OperationLogCreateSchema(**log_data), - auth=_auth, - ) - except Exception as e: - logger.warning(f"后台异步写入操作日志失败: {e}") - - -""" -在 FastAPI 中,route_class 参数用于自定义路由的行为。 -通过设置 route_class,你可以定义一个自定义的路由类,从而在每个路由处理之前或之后执行特定的操作。 -这对于日志记录、权限验证、性能监控等场景非常有用。 -""" + await OperationLogService(_auth).create(data=OperationLogCreateSchema(**log_data)) + except Exception: + logger.exception("操作日志写入失败: path=%s", log_data.get("request_path")) class OperationLogRoute(APIRoute): - """操作日志路由装饰器""" + """操作日志路由 — 自动记录请求/响应并后台异步写入""" - def get_route_handler( - self, - ) -> Callable[[Request], Coroutine[Any, Any, Response]]: - """ - 自定义路由处理程序,在每个路由处理之前或之后执行特定的操作。 - - 参数: - - request (Request): FastAPI请求对象。 - - 返回: - - Response: FastAPI响应对象。 - """ - original_route_handler = super().get_route_handler() + def get_route_handler(self) -> Callable[[Request], Coroutine[Any, Any, Response]]: + original = super().get_route_handler() async def custom_route_handler(request: Request) -> Response: - """ - 自定义路由处理程序,在每个路由处理之前或之后执行特定的操作。 + start = time.time() + response: Response = await original(request) - 参数: - - request (Request): FastAPI请求对象。 - 描述: - - 该方法在每个路由处理之前被调用,用于记录操作日志。 - 返回: - - Response: FastAPI响应对象。 - """ - start_time = time.time() - # 请求前的处理 - response: Response = await original_route_handler(request) - - # 请求后的处理(异步写入操作日志,不阻塞响应) - if not settings.OPERATION_LOG_RECORD: - return response - if request.method not in settings.OPERATION_RECORD_METHOD: + if not settings.OPERATION_LOG_RECORD or request.method not in settings.OPERATION_RECORD_METHOD: return response route: APIRoute = request.scope.get("route", None) if route and route.name in settings.IGNORE_OPERATION_FUNCTION: return response - # ── 收集日志数据(在请求上下文中同步完成)── try: - payload = b"{}" - req_content_type = request.headers.get("Content-Type", "") - oper_param: dict[str, Any] = {} - if req_content_type and req_content_type.startswith( - ("multipart/form-data", "application/x-www-form-urlencoded") - ): + content_type = request.headers.get("Content-Type", "") + if content_type.startswith(("multipart/form-data", "application/x-www-form-urlencoded")): form_data = await request.form() oper_param["form"] = dict(form_data.items()) else: @@ -100,44 +55,33 @@ class OperationLogRoute(APIRoute): except (json.JSONDecodeError, UnicodeDecodeError): oper_param["body"] = payload.decode("utf-8", errors="ignore") - path_params = request.path_params - if path_params: - oper_param["path_params"] = dict(path_params) + if request.path_params: + oper_param["path_params"] = dict(request.path_params) log_payload = json.dumps(oper_param, ensure_ascii=False) if len(log_payload) > 2000: log_payload = "请求参数过长" - response_data = ( - response.body - if "application/json" in response.headers.get("Content-Type", "") - else b"{}" - ) - process_time = f"{(time.time() - start_time):.2f}s" + is_json = "application/json" in response.headers.get("Content-Type", "") + response_data = response.body if is_json else b"{}" - current_user_id = request.scope.get("user_id") + ctx = getattr(request.state, "ctx", None) + current_user_id = ctx.user_id if ctx else None - # 构造日志数据字典,传给后台任务 log_data: dict[str, Any] = { "request_path": request.url.path, "request_method": request.method, "request_payload": log_payload, "response_code": response.status_code, - "response_json": ( - response_data.decode() - if isinstance(response_data, (bytes, bytearray)) - else str(response_data) - ), - "process_time": process_time, + "response_json": response_data.decode(), + "process_time": f"{(time.time() - start):.2f}s", "description": route.summary if route else "", "created_id": current_user_id, "updated_id": current_user_id, } - - # ── 后台异步写入,不阻塞响应 ── - asyncio.create_task(_write_operation_log_async(log_data)) + response.background = BackgroundTask(_write_operation_log_async, log_data) except Exception: - logger.warning(f"操作日志采集异常: {request.url.path}", exc_info=True) + logger.warning("操作日志采集异常: %s", request.url.path, exc_info=True) return response return custom_route_handler diff --git a/backend/app/core/tenant.py b/backend/app/core/tenant.py deleted file mode 100644 index 47b9084f..00000000 --- a/backend/app/core/tenant.py +++ /dev/null @@ -1,54 +0,0 @@ -"""多租户上下文管理 - -使用 Python contextvars 实现异步安全的租户上下文传递。 -在整个请求生命周期中,通过 ContextVar 存储当前请求的租户 ID -和超级管理员标识,供 ORM 事件处理器、依赖注入等模块使用。 -""" - -from __future__ import annotations - -from contextvars import ContextVar - -# 当前请求的租户 ID(None 表示未设置或公开接口) -current_tenant_id: ContextVar[int | None] = ContextVar("current_tenant_id", default=None) - -# 当前用户是否为超级管理员 -current_is_super_admin: ContextVar[bool] = ContextVar("current_is_super_admin", default=False) - - -def set_current_tenant(tenant_id: int | None, is_super_admin: bool = False) -> None: - """设置当前请求的租户上下文。 - - 参数: - tenant_id: 租户 ID,None 表示无租户上下文(公开接口或未认证)。 - is_super_admin: 是否为平台超级管理员,默认 False。 - """ - current_tenant_id.set(tenant_id) - current_is_super_admin.set(is_super_admin) - - -def get_current_tenant_id() -> int | None: - """获取当前请求的租户 ID。 - - 返回: - int | None: 租户 ID,未设置时返回 None。 - """ - return current_tenant_id.get() - - -def get_is_super_admin() -> bool: - """获取当前用户是否为超级管理员。 - - 返回: - bool: 是否为超级管理员,默认 False。 - """ - return current_is_super_admin.get() - - -def clear_current_tenant() -> None: - """清除当前请求的租户上下文(重置为默认值)。 - - 应在请求结束时调用,防止 ContextVar 泄漏到其他请求。 - """ - current_tenant_id.set(None) - current_is_super_admin.set(False) diff --git a/backend/app/core/tenant_filter.py b/backend/app/core/tenant_filter.py deleted file mode 100644 index f10e76d3..00000000 --- a/backend/app/core/tenant_filter.py +++ /dev/null @@ -1,226 +0,0 @@ -"""多租户 ORM 查询过滤器 - -通过 SQLAlchemy 的 ``do_orm_execute`` 事件,在 ORM 层自动注入租户过滤条件。 - -核心逻辑: -- SELECT: 如果实体映射的表有 ``tenant_id`` 列且当前用户不是超级管理员, - 自动追加 ``WHERE tenant_id = current_tenant_id``。 -- INSERT: 如果表有 ``tenant_id`` 列,自动设置值为 ``current_tenant_id``。 -- UPDATE/DELETE: 如果表有 ``tenant_id`` 列且非超管, - 自动追加 ``WHERE tenant_id = current_tenant_id``。 -- 超管(``is_super_admin=True``)时跳过所有 tenant_id 过滤。 -""" - -from __future__ import annotations - -import logging - -from sqlalchemy import event, inspect -from sqlalchemy.orm import Mapper, Session -from sqlalchemy.sql import Delete, Insert, Select, Update - -from app.core.tenant import get_current_tenant_id, get_is_super_admin - -logger = logging.getLogger(__name__) - -# 系统表名集合(不对这些表进行租户过滤,避免死循环) -_SYSTEM_TABLE_NAMES: frozenset[str] = frozenset({"platform_tenant"}) - -_registered = False - - -def _has_tenant_column(mapper_or_entity) -> bool: - """检查映射类或实体是否有 ``tenant_id`` 列。 - - 参数: - mapper_or_entity: SQLAlchemy Mapper 对象或映射类。 - - 返回: - bool: 是否包含 tenant_id 列。 - """ - try: - mapper = inspect(mapper_or_entity) - if isinstance(mapper, Mapper): - return "tenant_id" in mapper.columns - return False - except Exception: - return False - - -def _is_system_table(table) -> bool: - """判断是否为系统表(不应被租户过滤)。 - - 参数: - table: SQLAlchemy Table 对象。 - - 返回: - bool: 是否为系统表。 - """ - return table.name in _SYSTEM_TABLE_NAMES - - -def _collect_tenant_mappers(execute_state) -> set[Mapper]: - """从 ORM 执行状态中收集所有包含 tenant_id 列的 Mapper。 - - 参数: - execute_state: SQLAlchemy ORMExecuteState 对象。 - - 返回: - set[Mapper]: 需要租户过滤的 Mapper 集合。 - """ - tenant_mappers: set[Mapper] = set() - - # 检查绑定的 Mapper(单实体查询) - if execute_state.bind_mapper and _has_tenant_column(execute_state.bind_mapper): - mapper = inspect(execute_state.bind_mapper) - if not _is_system_table(mapper.mapped_table): - # 跳过 __platform_data_shared__ 模型(由 CRUD 层处理共享逻辑) - if not getattr(mapper.class_, "__platform_data_shared__", False): - tenant_mappers.add(mapper) - - # 检查语句中所有实体(多实体查询 / join) - stmt = execute_state.statement - if hasattr(stmt, "column_descriptions"): - for desc in stmt.column_descriptions: - entity = desc.get("entity") - if entity is not None and _has_tenant_column(entity): - mapper = inspect(entity) - if not _is_system_table(mapper.mapped_table): - # 跳过 __platform_data_shared__ 模型 - if not getattr(mapper.class_, "__platform_data_shared__", False): - tenant_mappers.add(mapper) - - return tenant_mappers - - -def _apply_tenant_to_select(execute_state, tenant_id: int) -> None: - """为 SELECT 语句添加租户过滤条件。 - - 对每个包含 tenant_id 列的映射表,追加 ``WHERE tenant_id = ?``。 - - 参数: - execute_state: ORMExecuteState 对象。 - tenant_id: 当前租户 ID。 - """ - tenant_mappers = _collect_tenant_mappers(execute_state) - if not tenant_mappers: - return - - stmt = execute_state.statement - for mapper in tenant_mappers: - table = mapper.mapped_table - stmt = stmt.where(table.c.tenant_id == tenant_id) - - execute_state.statement = stmt - - -def _apply_tenant_to_insert(execute_state, tenant_id: int) -> None: - """为 INSERT 语句设置 tenant_id 值。 - - 如果插入语句未显式指定 tenant_id,则自动设置为当前租户 ID。 - - 参数: - execute_state: ORMExecuteState 对象。 - tenant_id: 当前租户 ID。 - """ - stmt: Insert = execute_state.statement - - if not hasattr(stmt, "table"): - return - - table = stmt.table - if not _has_tenant_column(table) or _is_system_table(table): - return - - # 检查是否已经显式设置了 tenant_id - compiled_params = stmt.compile(bind=execute_state.session.bind) - for params in compiled_params.construct_params(): - if "tenant_id" in params: - return # 已显式设置,不覆盖 - - # 自动注入 tenant_id - stmt = stmt.values(tenant_id=tenant_id) - execute_state.statement = stmt - - -def _apply_tenant_to_update(execute_state, tenant_id: int) -> None: - """为 UPDATE 语句添加租户过滤条件。 - - 参数: - execute_state: ORMExecuteState 对象。 - tenant_id: 当前租户 ID。 - """ - stmt: Update = execute_state.statement - - if not hasattr(stmt, "table"): - return - - table = stmt.table - if not _has_tenant_column(table) or _is_system_table(table): - return - - stmt = stmt.where(table.c.tenant_id == tenant_id) - execute_state.statement = stmt - - -def _apply_tenant_to_delete(execute_state, tenant_id: int) -> None: - """为 DELETE 语句添加租户过滤条件。 - - 参数: - execute_state: ORMExecuteState 对象。 - tenant_id: 当前租户 ID。 - """ - stmt: Delete = execute_state.statement - - if not hasattr(stmt, "table"): - return - - table = stmt.table - if not _has_tenant_column(table) or _is_system_table(table): - return - - stmt = stmt.where(table.c.tenant_id == tenant_id) - execute_state.statement = stmt - - -@event.listens_for(Session, "do_orm_execute") -def tenant_orm_filter(execute_state) -> None: - """SQLAlchemy ``do_orm_execute`` 事件处理器。 - - 在每次 ORM 执行前自动注入租户过滤条件。 - 通过 ContextVar 获取当前请求的租户上下文, - 超级管理员或未设置租户上下文时跳过过滤。 - - 参数: - execute_state: ORMExecuteState 对象。 - """ - # 跳过列加载和关系加载(内部操作) - if execute_state.is_column_load or execute_state.is_relationship_load: - return - - # 获取当前租户上下文 - tenant_id = get_current_tenant_id() - is_super_admin = get_is_super_admin() - - # 超级管理员不过滤;未设置租户上下文时不过滤 - if is_super_admin or tenant_id is None: - return - - stmt = execute_state.statement - - try: - if isinstance(stmt, Select): - _apply_tenant_to_select(execute_state, tenant_id) - elif isinstance(stmt, Insert): - _apply_tenant_to_insert(execute_state, tenant_id) - elif isinstance(stmt, Update): - _apply_tenant_to_update(execute_state, tenant_id) - elif isinstance(stmt, Delete): - _apply_tenant_to_delete(execute_state, tenant_id) - except Exception: - # 过滤异常时记录日志但不中断业务逻辑 - logger.exception( - "租户 ORM 过滤异常: statement_type=%s, tenant_id=%s", - type(stmt).__name__, - tenant_id, - ) diff --git a/backend/app/init_app.py b/backend/app/init_app.py index 31e8a5fb..eb24e451 100644 --- a/backend/app/init_app.py +++ b/backend/app/init_app.py @@ -1,14 +1,9 @@ - from collections.abc import AsyncGenerator from typing import Any from fastapi import Depends, FastAPI from fastapi.concurrency import asynccontextmanager -from fastapi.openapi.docs import ( - get_redoc_html, - get_swagger_ui_html, - get_swagger_ui_oauth2_redirect_html, -) +from fastapi.openapi.docs import get_redoc_html, get_swagger_ui_html, get_swagger_ui_oauth2_redirect_html from fastapi.responses import HTMLResponse from fastapi.staticfiles import StaticFiles from fastapi_cache import FastAPICache @@ -22,20 +17,11 @@ from .core.http_limit import http_limit_callback, ws_limit_callback from .core.logger import logger from .scripts.initialize import InitializeData from .utils.common_util import import_module, import_modules_async -from .utils.console import console_close, console_run +from .utils.console import console_end, console_start @asynccontextmanager async def lifespan(app: FastAPI) -> AsyncGenerator[Any, Any]: - """ - 自定义 FastAPI 应用生命周期。 - - 参数: - - app (FastAPI): FastAPI 应用实例。 - - 返回: - - AsyncGenerator[Any, Any]: 生命周期上下文生成器。 - """ from app.api.v1.module_platform.tenant.service import TenantService from app.api.v1.module_system.dict.service import DictDataService from app.api.v1.module_system.params.service import ParamsService @@ -43,16 +29,14 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[Any, Any]: try: await InitializeData().init_db() - logger.info(f"✅ {settings.DATABASE_TYPE}数据库初始化完成") - await import_modules_async( - modules=settings.EVENT_LIST, desc="全局事件", app=app, status=True - ) + logger.info("✅ %s数据库初始化完成", settings.DATABASE_TYPE) + await import_modules_async(modules=settings.EVENT_LIST, desc="全局事件", app=app, status=True) logger.info("✅ 全局事件模块加载完成") - await ParamsService.init_config_service(redis=app.state.redis) - logger.info("✅ Redis系统配置初始化完成") - await DictDataService.init_dict_service(redis=app.state.redis) + await ParamsService.init_cache(redis=app.state.redis) + logger.info("✅ Redis系统参数初始化完成") + await DictDataService.init_cache(redis=app.state.redis) logger.info("✅ Redis数据字典初始化完成") - await TenantService.init_tenant_config_cache(redis=app.state.redis) + await TenantService.init_cache(redis=app.state.redis) logger.info("✅ Redis租户配置初始化完成") await SchedulerUtil.init_scheduler(redis=app.state.redis) logger.info("✅ 定时任务调度器初始化完成") @@ -63,30 +47,17 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[Any, Any]: ws_callback=ws_limit_callback, ) logger.info("✅ 请求限流器初始化完成") - # fastapi-cache2 初始化:复用 app.state.redis 主连接 - FastAPICache.init( - backend=RedisBackend(app.state.redis), - prefix="fastapi-admin-cache", - expire=300, # 默认 5 分钟 - enable=True, - ) + FastAPICache.init(backend=RedisBackend(app.state.redis), prefix="fastapi-admin-cache", expire=300, enable=True) logger.info("✅ fastapi-cache2 初始化完成") - # 导入并显示最终的启动信息面板 - from app.common.enums import EnvironmentEnum - - console_run( - host=settings.SERVER_HOST, - port=settings.SERVER_PORT, - reload=settings.ENVIRONMENT == EnvironmentEnum.DEV, - database_ready=True, - redis_ready=True, - scheduler_ready=SchedulerUtil.is_running(), - limiter_ready=True, + console_start( + host=settings.SERVER_HOST, port=settings.SERVER_PORT, + reload=settings.ENVIRONMENT, + database_ready=True, redis_ready=True, + scheduler_ready=SchedulerUtil.is_running(), limiter_ready=True, ) - except Exception as e: - logger.error(f"❌ 应用初始化失败: {e!s}") + logger.error("❌ 应用初始化失败: %s", e) raise SystemExit(1) yield @@ -103,128 +74,59 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[Any, Any]: from app.core.database import async_engine await async_engine.dispose() logger.info("✅ 数据库引擎连接池已释放") - - console_close() - + console_end() except Exception as e: - logger.error(f"❌ 应用关闭过程中发生错误: {e!s}") + logger.error("❌ 应用关闭过程中发生错误: %s", e) + raise SystemExit(1) def register_middlewares(app: FastAPI) -> None: - """ - 注册全局中间件。 - - 参数: - - app (FastAPI): FastAPI 应用实例。 - - 返回: - - None - """ - for middleware in settings.MIDDLEWARE_LIST[::-1]: if not middleware: continue middleware = import_module(middleware, desc="中间件") app.add_middleware(middleware) - # 注册多租户 ORM 过滤器(导入即触发 @event.listens_for 注册) - import app.core.tenant_filter # noqa: F401 - def register_exceptions(app: FastAPI) -> None: - """ - 统一注册异常处理器。 - - 参数: - - app (FastAPI): FastAPI 应用实例。 - - 返回: - - None - """ handle_exception(app) def register_routers(app: FastAPI) -> None: - """ - 注册根路由。 - - 参数: - - app (FastAPI): FastAPI 应用实例。 - - 返回: - - None - """ from app.api.v1.module_common import common_router from app.api.v1.module_monitor import monitor_router from app.api.v1.module_platform import platform_router from app.api.v1.module_system import system_router - - # 业务路由(带速率限制) - app.include_router(common_router, dependencies=[Depends(RateLimiter(times=200, seconds=10))]) - app.include_router(system_router, dependencies=[Depends(RateLimiter(times=200, seconds=10))]) - app.include_router(platform_router, dependencies=[Depends(RateLimiter(times=200, seconds=10))]) - app.include_router(monitor_router, dependencies=[Depends(RateLimiter(times=200, seconds=10))]) - + from app.core.discover import get_dynamic_router, set_app_ref from app.plugin.module_ai.chat.ws import WS_AI - # 手动注册WebSocket路由,不使用速率限制器 - app.include_router( - router=WS_AI, dependencies=[Depends(WebSocketRateLimiter(times=1, seconds=5))] - ) - # 先将动态路由注册到应用,使用速率限制器 - from app.core.discover import get_dynamic_router, set_app_ref + for router in (common_router, system_router, platform_router, monitor_router): + app.include_router(router, dependencies=[Depends(RateLimiter(times=settings.RATE_LIMITER_TIMES, seconds=settings.RATE_LIMITER_SECONDS))]) - # 获取动态路由实例 - app.include_router( - router=get_dynamic_router(), - dependencies=[Depends(RateLimiter(times=200, seconds=10))], - ) - # 保存 app 引用,供热重载时操作 app.routes + app.include_router(router=get_dynamic_router(), dependencies=[Depends(RateLimiter(times=settings.RATE_LIMITER_TIMES, seconds=settings.RATE_LIMITER_SECONDS))]) set_app_ref(app) - logger.info("✅ 应用启动完成") + app.include_router(router=WS_AI, dependencies=[Depends(WebSocketRateLimiter(times=settings.WS_RATE_LIMITER_TIMES, seconds=settings.WS_RATE_LIMITER_SECONDS))]) def register_files(app: FastAPI) -> None: - """ - 注册静态资源挂载和文件相关配置。 - - 参数: - - app (FastAPI): FastAPI 应用实例。 - - 返回: - - None - """ - # 挂载静态文件目录 if settings.STATIC_ENABLE: - # 确保静态资源根目录存在 settings.STATIC_ROOT.mkdir(parents=True, exist_ok=True) - app.mount( - path=settings.STATIC_URL, - app=StaticFiles(directory=settings.STATIC_ROOT), - name=settings.STATIC_DIR, - ) + app.mount(path=settings.STATIC_URL, app=StaticFiles(directory=settings.STATIC_ROOT), name=settings.STATIC_DIR) def reset_api_docs(app: FastAPI) -> None: - """ - 使用本地静态资源自定义 API 文档页面(Swagger UI 与 ReDoc)。 + swagger_ui_redirect_url = str(app.swagger_ui_oauth2_redirect_url) + root_openapi_url = str(app.root_path) + str(app.openapi_url) - 参数: - - app (FastAPI): FastAPI 应用实例。 - - 返回: - - None - """ - - @app.get(str(app.swagger_ui_oauth2_redirect_url), include_in_schema=False) + @app.get(swagger_ui_redirect_url, include_in_schema=False) async def swagger_ui_redirect(): return get_swagger_ui_oauth2_redirect_html() @app.get(settings.DOCS_URL, include_in_schema=False) async def custom_swagger_ui_html() -> HTMLResponse: return get_swagger_ui_html( - openapi_url=str(app.root_path) + str(app.openapi_url), + openapi_url=root_openapi_url, title=app.title + " - Swagger UI", oauth2_redirect_url=app.swagger_ui_oauth2_redirect_url, swagger_js_url=settings.SWAGGER_JS_URL, @@ -235,7 +137,7 @@ def reset_api_docs(app: FastAPI) -> None: @app.get(settings.REDOC_URL, include_in_schema=False) async def custom_redoc_html(): return get_redoc_html( - openapi_url=str(app.root_path) + str(app.openapi_url), + openapi_url=root_openapi_url, title=app.title + " - ReDoc", redoc_js_url=settings.REDOC_JS_URL, redoc_favicon_url=settings.FAVICON_URL, diff --git a/backend/app/plugin/module_ai/chat/controller.py b/backend/app/plugin/module_ai/chat/controller.py index 94a6a984..d964b995 100644 --- a/backend/app/plugin/module_ai/chat/controller.py +++ b/backend/app/plugin/module_ai/chat/controller.py @@ -18,7 +18,7 @@ from .schema import ( ) from .service import ChatService -ChatRouter = APIRouter(route_class=OperationLogRoute, prefix="/chat", tags=["AI对话"]) +ChatRouter = APIRouter(route_class=OperationLogRoute, prefix="/chat", tags=["AI管理", "AI对话"]) @ChatRouter.get( @@ -30,17 +30,8 @@ async def get_session_detail_controller( session_id: Annotated[str, Path(description="会话ID")], auth: Annotated[AuthSchema, Depends(AuthPermission(["module_ai:chat:detail"]))], ) -> JSONResponse: - """ - 获取会话详情 - - 参数: - - session_id (str): 会话ID - - auth (AuthSchema): 认证信息模型 - - 返回: - - JSONResponse: 包含会话详情的JSON响应 - """ - result = await ChatService.get_session_service(auth=auth, session_id=session_id) + service = ChatService(auth) + result = await service.get_session(session_id=session_id) return SuccessResponse(data=result, msg="获取会话详情成功") @@ -54,19 +45,8 @@ async def get_session_list_controller( search: Annotated[ChatSessionQueryParam, Depends()], auth: Annotated[AuthSchema, Depends(AuthPermission(["module_ai:chat:query"]))], ) -> JSONResponse: - """ - 查询会话列表 - - 参数: - - page (PaginationQueryParam): 分页查询参数 - - search (ChatSessionQueryParam): 查询参数 - - auth (AuthSchema): 认证信息模型 - - 返回: - - JSONResponse: 包含会话列表分页信息的JSON响应 - """ - result_dict = await ChatService.page_service( - auth=auth, + service = ChatService(auth) + result_dict = await service.page( page_no=page.page_no, page_size=page.page_size, search=search, @@ -84,17 +64,8 @@ async def create_session_controller( data: ChatSessionCreateSchema, auth: Annotated[AuthSchema, Depends(AuthPermission(["module_ai:chat:create"]))], ) -> JSONResponse: - """ - 创建会话 - - 参数: - - data (ChatSessionCreateSchema): 会话创建模型 - - auth (AuthSchema): 认证信息模型 - - 返回: - - JSONResponse: 包含创建会话详情的JSON响应 - """ - result = await ChatService.create_service(auth=auth, data=data) + service = ChatService(auth) + result = await service.create(data=data) return SuccessResponse(data=result, msg="创建会话成功") @@ -108,18 +79,8 @@ async def update_session_controller( data: ChatSessionUpdateSchema, auth: Annotated[AuthSchema, Depends(AuthPermission(["module_ai:chat:update"]))], ) -> JSONResponse: - """ - 更新会话 - - 参数: - - session_id (str): 会话ID - - data (ChatSessionUpdateSchema): 会话更新模型 - - auth (AuthSchema): 认证信息模型 - - 返回: - - JSONResponse: 包含更新会话详情的JSON响应 - """ - await ChatService.update_service(auth=auth, session_id=session_id, data=data) + service = ChatService(auth) + await service.update(session_id=session_id, data=data) return SuccessResponse(data=None, msg="更新会话成功") @@ -132,17 +93,8 @@ async def delete_session_controller( session_ids: list[str], auth: Annotated[AuthSchema, Depends(AuthPermission(["module_ai:chat:delete"]))], ) -> JSONResponse: - """ - 删除会话 - - 参数: - - session_ids (list[str]): 会话ID列表 - - auth (AuthSchema): 认证信息模型 - - 返回: - - JSONResponse: 包含删除结果的JSON响应 - """ - await ChatService.delete_service(auth=auth, session_ids=session_ids) + service = ChatService(auth) + await service.delete(session_ids=session_ids) return SuccessResponse(data=None, msg="删除会话成功") @@ -155,20 +107,10 @@ async def ai_chat_controller( data: AiChatRequestSchema, auth: Annotated[AuthSchema, Depends(AuthPermission(["module_ai:chat:query"]))], ) -> JSONResponse: - """ - AI 对话(非流式) - - 参数: - - data (AiChatRequestSchema): 对话请求数据 - - auth (AuthSchema): 认证信息模型 - - 返回: - - JSONResponse: 包含 AI 回复、会话ID和函数调用信息的JSON响应 - """ - result = await ChatService.chat_non_stream( + service = ChatService(auth) + result = await service.chat_non_stream( message=data.message, session_id=data.session_id, - auth=auth, ) return SuccessResponse( data=AiChatResponseSchema( diff --git a/backend/app/plugin/module_ai/chat/service.py b/backend/app/plugin/module_ai/chat/service.py index 56a0b7c6..e4b86521 100644 --- a/backend/app/plugin/module_ai/chat/service.py +++ b/backend/app/plugin/module_ai/chat/service.py @@ -66,10 +66,10 @@ async def _format_session_data(session: TeamSession, auth: AuthSchema | None = N try: team_id = session_dict.get("team_id") if isinstance(team_id, str): - dept_name = await DeptService.detail_service(auth=auth, id=int(team_id)) + dept_name = await DeptService(auth).detail(id=int(team_id)) result["team_name"] = dept_name.get("name") elif isinstance(team_id, int): - dept_name = await DeptService.detail_service(auth=auth, id=team_id) + dept_name = await DeptService(auth).detail(id=team_id) result["team_name"] = dept_name.get("name") else: result["team_name"] = None @@ -128,26 +128,16 @@ def _extract_messages(runs: list[dict[str, Any]]) -> list[dict[str, Any]]: class ChatService: """聊天会话管理模块服务层""" - @classmethod - async def chat_query(cls, query: ChatQuerySchema, auth: AuthSchema) -> AsyncGenerator[str, None]: - """ - 处理聊天查询并流式返回文本片段。 + def __init__(self, auth: AuthSchema) -> None: + self.auth = auth - 参数: - - query (ChatQuerySchema): 用户消息与会话等查询参数。 - - auth (AuthSchema): 当前用户认证信息。 - - 返回: - - AsyncGenerator[str, None]: 逐段输出的回复文本。 - """ + async def chat_query(self, query: ChatQuerySchema) -> AsyncGenerator[str, None]: + """流式 AI 对话""" try: - # 创建 CRUD 实例获取数据库连接 - crud = ChatSessionCRUD(auth) + crud = ChatSessionCRUD(self.auth) - # 获取或创建会话 session_id = query.session_id if not session_id: - # 创建新会话 import uuid session_id = str(uuid.uuid4()) @@ -156,17 +146,15 @@ class ChatService: raise CustomException(msg="创建会话失败") session_id = session.session_id - # 创建 AgnoFactory 实例并创建 Team,传入数据库连接 agno_factory = AgnoFactory() - dept_id = str(auth.user.dept_id) if auth and auth.user and hasattr(auth.user, "dept_id") and auth.user.dept_id else "default" + dept_id = str(self.auth.user.dept_id) if self.auth and self.auth.user and hasattr(self.auth.user, "dept_id") and self.auth.user.dept_id else "default" agent = agno_factory.create_agent( - user_id=auth.user.username if auth and auth.user else "user", + user_id=self.auth.user.username if self.auth and self.auth.user else "user", dept_id=dept_id, session_id=session_id, db=crud.db, ) - # 执行聊天查询 - 使用流式输出 async for chunk in agent.arun(input=query.message, stream=True): if chunk and chunk.content: yield chunk.content @@ -175,26 +163,12 @@ class ChatService: logger.error(f"聊天查询失败: {e}") yield f"抱歉,处理您的请求时出现错误:{str(e)}" - @classmethod - async def chat_non_stream(cls, message: str, session_id: str | None, auth: AuthSchema) -> dict[str, Any]: - """ - 处理聊天查询并返回非流式 JSON 结构(含 session_id、操作建议等)。 - - 参数: - - message (str): 用户输入文本。 - - session_id (str | None): 已有会话 ID;为空则新建会话。 - - auth (AuthSchema): 当前用户认证信息。 - - 返回: - - dict[str, Any]: 包含 response、session_id、action 等字段的字典。 - """ + async def chat_non_stream(self, message: str, session_id: str | None) -> dict[str, Any]: + """非流式 AI 对话""" try: - # 创建 CRUD 实例获取数据库连接 - crud = ChatSessionCRUD(auth) + crud = ChatSessionCRUD(self.auth) - # 获取或创建会话 if not session_id: - # 创建新会话 import uuid session_id = str(uuid.uuid4()) @@ -203,35 +177,28 @@ class ChatService: raise CustomException(msg="创建会话失败") session_id = session.session_id - # 创建 AgnoFactory 实例并创建 Team,传入数据库连接 agno_factory = AgnoFactory() - dept_id = str(auth.user.dept_id) if auth and auth.user and hasattr(auth.user, "dept_id") and auth.user.dept_id else "default" + dept_id = str(self.auth.user.dept_id) if self.auth and self.auth.user and hasattr(self.auth.user, "dept_id") and self.auth.user.dept_id else "default" agent: Team = agno_factory.create_agent( - user_id=auth.user.username if auth and auth.user else "user", + user_id=self.auth.user.username if self.auth and self.auth.user else "user", dept_id=dept_id, session_id=session_id, db=crud.db, ) - # 执行聊天查询 response: TeamRunOutput = await agent.arun(input=message) - # 解析响应内容和操作建议 response_text = "" action = None if response and response.content: response_text = response.content - # 尝试从 response 中解析操作建议 - # 如果 AI 返回了 JSON 格式的操作建议 import json try: - # 检查响应是否包含 JSON 格式的操作建议 if response_text.strip().startswith("{") and response_text.strip().endswith("}"): action = json.loads(response_text) elif "```json" in response_text: - # 提取 JSON 代码块 json_start = response_text.find("```json") + 7 json_end = response_text.find("```", json_start) if json_end > json_start: @@ -240,9 +207,8 @@ class ChatService: except (json.JSONDecodeError, Exception): pass - # 如果没有解析到 JSON,尝试从文本中提取操作信息 if not action: - action = cls._parse_action_from_response(response_text) + action = self._parse_action_from_response(response_text) return { "response": response_text, @@ -264,7 +230,6 @@ class ChatService: def _parse_action_from_response(response_text: str) -> dict[str, Any] | None: """从响应文本中解析操作建议""" - # 定义路由配置 route_config = { "用户管理": {"path": "/system/user", "name": "用户管理"}, "角色管理": {"path": "/system/role", "name": "角色管理"}, @@ -274,14 +239,12 @@ class ChatService: "系统日志": {"path": "/system/log", "name": "系统日志"}, } - # 检查是否包含导航意图 navigation_keywords = ["跳转", "打开", "进入", "前往", "去", "浏览", "查看"] has_navigation = any(keyword in response_text for keyword in navigation_keywords) if not has_navigation: return None - # 查找页面名称 for page_name, route_info in route_config.items(): if page_name in response_text: return { @@ -290,7 +253,6 @@ class ChatService: "name": route_info["name"], } - # 尝试从关键词匹配 keyword_mapping = { "用户": {"path": "/system/user", "name": "用户管理"}, "角色": {"path": "/system/role", "name": "角色管理"}, @@ -310,107 +272,39 @@ class ChatService: return None - @classmethod - async def create_service(cls, auth: AuthSchema, data: ChatSessionCreateSchema) -> dict[str, Any] | None: - """ - 创建会话。 - - 参数: - - auth (AuthSchema): 认证信息。 - - data (ChatSessionCreateSchema): 创建参数。 - - 返回: - - dict[str, Any] | None: 格式化后的会话字典;失败为 None。 - """ - crud = ChatSessionCRUD(auth) - session = await crud.create_crud(data=data) - if session: - return await _format_session_data(session, auth) - return None - - @classmethod - async def get_session_service(cls, auth: AuthSchema, session_id: str) -> dict[str, Any] | None: - """ - 获取单个会话详情。 - - 参数: - - auth (AuthSchema): 认证信息。 - - session_id (str): 会话 ID。 - - 返回: - - dict[str, Any] | None: 格式化后的会话字典;不存在为 None。 - """ - crud = ChatSessionCRUD(auth) + async def get_session(self, session_id: str) -> dict[str, Any] | None: + crud = ChatSessionCRUD(self.auth) session: TeamSession | None = await crud.get_by_id_crud(session_id=session_id) if session: - return await _format_session_data(session, auth) + return await _format_session_data(session, self.auth) return None - @classmethod - async def page_service( - cls, - auth: AuthSchema, + async def create(self, data: ChatSessionCreateSchema) -> dict[str, Any] | None: + crud = ChatSessionCRUD(self.auth) + session = await crud.create_crud(data=data) + if session: + return await _format_session_data(session, self.auth) + return None + + async def page( + self, page_no: int, page_size: int, search: ChatSessionQueryParam, order_by: list[dict[str, str]] | None = None, ) -> dict[str, Any]: - """ - 分页获取会话列表。会话由 Agno 存储,无统一 SQL 分页,仅对内存列表切片。 - - 参数: - - auth (AuthSchema): 认证信息。 - - page_no (int): 页码。 - - page_size (int): 每页条数。 - - search (ChatSessionQueryParam): 查询条件。 - - order_by (list[dict[str, str]] | None): 排序,可选。 - - 返回: - - dict[str, Any]: 分页结果(含 items、total 等)。 - """ - crud = ChatSessionCRUD(auth) - # 获取所有会话 + crud = ChatSessionCRUD(self.auth) sessions = await crud.list_crud() - - # 转换为响应模型 - 使用 TeamSession 内置的 to_dict 方法并格式化 - items = [await _format_session_data(s, auth) for s in sessions] - - # 非关系型会话存储,沿用内存分页 - result = await PaginationService.paginate( + items = [await _format_session_data(s, self.auth) for s in sessions] + return await PaginationService.paginate( data_list=items, page_no=page_no, page_size=page_size, ) - return result + async def update(self, session_id: str, data: ChatSessionUpdateSchema) -> bool: + crud = ChatSessionCRUD(self.auth) + return await crud.update_crud(session_id=session_id, data=data) - @classmethod - async def update_service(cls, auth: AuthSchema, session_id: str, data: ChatSessionUpdateSchema) -> bool: - """ - 更新会话。 - - 参数: - - auth (AuthSchema): 认证信息。 - - session_id (str): 会话 ID。 - - data (ChatSessionUpdateSchema): 更新数据。 - - 返回: - - bool: 是否成功。 - """ - crud = ChatSessionCRUD(auth) - success = await crud.update_crud(session_id=session_id, data=data) - return success - - @classmethod - async def delete_service(cls, auth: AuthSchema, session_ids: list[str]) -> None: - """ - 删除会话。 - - 参数: - - auth (AuthSchema): 认证信息。 - - session_ids (list[str]): 待删除会话 ID 列表。 - - 返回: - - None - """ - await ChatSessionCRUD(auth).delete_crud(session_ids=session_ids) + async def delete(self, session_ids: list[str]) -> None: + await ChatSessionCRUD(self.auth).delete_crud(session_ids=session_ids) diff --git a/backend/app/plugin/module_example/demo/controller.py b/backend/app/plugin/module_example/demo/controller.py index 59fe427c..e74e6295 100644 --- a/backend/app/plugin/module_example/demo/controller.py +++ b/backend/app/plugin/module_example/demo/controller.py @@ -14,7 +14,7 @@ from app.utils.common_util import bytes2file_response from .schema import DemoCreateSchema, DemoOutSchema, DemoQueryParam, DemoUpdateSchema from .service import DemoService -DemoRouter = APIRouter(route_class=OperationLogRoute, prefix="/demo", tags=["开发工具/示例"]) +DemoRouter = APIRouter(route_class=OperationLogRoute, prefix="/demo", tags=["开发工具", "示例"]) @DemoRouter.get( @@ -26,17 +26,8 @@ async def get_obj_detail_controller( id: Annotated[int, Path(description="示例ID")], auth: Annotated[AuthSchema, Depends(AuthPermission(["module_example:demo:detail"]))], ) -> JSONResponse: - """ - 获取示例详情 - - 参数: - - id (int): 示例ID - - auth (AuthSchema): 认证信息模型 - - 返回: - - JSONResponse: 包含示例详情的JSON响应 - """ - result_dict = await DemoService.detail_service(id=id, auth=auth) + service = DemoService(auth) + result_dict = await service.detail(id=id) return SuccessResponse(data=result_dict, msg="获取示例详情成功") @@ -50,20 +41,8 @@ async def get_obj_list_controller( search: Annotated[DemoQueryParam, Depends()], auth: Annotated[AuthSchema, Depends(AuthPermission(["module_example:demo:query"]))], ) -> JSONResponse: - """ - 查询示例列表 - - 参数: - - page (PaginationQueryParam): 分页查询参数 - - search (DemoQueryParam): 查询参数 - - auth (AuthSchema): 认证信息模型 - - 返回: - - JSONResponse: 包含示例列表分页信息的JSON响应 - """ - # 使用数据库分页而不是应用层分页 - result_dict = await DemoService.page_service( - auth=auth, + service = DemoService(auth) + result_dict = await service.page( page_no=page.page_no, page_size=page.page_size, search=search, @@ -81,17 +60,8 @@ async def create_obj_controller( data: DemoCreateSchema, auth: Annotated[AuthSchema, Depends(AuthPermission(["module_example:demo:create"]))], ) -> JSONResponse: - """ - 创建示例 - - 参数: - - data (DemoCreateSchema): 示例创建模型 - - auth (AuthSchema): 认证信息模型 - - 返回: - - JSONResponse: 包含创建示例详情的JSON响应 - """ - result_dict = await DemoService.create_service(auth=auth, data=data) + service = DemoService(auth) + result_dict = await service.create(data=data) return SuccessResponse(data=result_dict, msg="创建示例成功") @@ -105,18 +75,8 @@ async def update_obj_controller( id: Annotated[int, Path(description="示例ID")], auth: Annotated[AuthSchema, Depends(AuthPermission(["module_example:demo:update"]))], ) -> JSONResponse: - """ - 修改示例 - - 参数: - - data (DemoUpdateSchema): 示例更新模型 - - id (int): 示例ID - - auth (AuthSchema): 认证信息模型 - - 返回: - - JSONResponse: 包含修改示例详情的JSON响应 - """ - result_dict = await DemoService.update_service(auth=auth, id=id, data=data) + service = DemoService(auth) + result_dict = await service.update(id=id, data=data) return SuccessResponse(data=result_dict, msg="修改示例成功") @@ -129,17 +89,8 @@ async def delete_obj_controller( ids: Annotated[list[int], Body(description="ID列表")], auth: Annotated[AuthSchema, Depends(AuthPermission(["module_example:demo:delete"]))], ) -> JSONResponse: - """ - 删除示例 - - 参数: - - ids (list[int]): 示例ID列表 - - auth (AuthSchema): 认证信息模型 - - 返回: - - JSONResponse: 包含删除示例详情的JSON响应 - """ - await DemoService.delete_service(auth=auth, ids=ids) + service = DemoService(auth) + await service.delete(ids=ids) return SuccessResponse(msg="删除示例成功") @@ -152,17 +103,8 @@ async def batch_set_available_obj_controller( data: BatchSetAvailable, auth: Annotated[AuthSchema, Depends(AuthPermission(["module_example:demo:patch"]))], ) -> JSONResponse: - """ - 批量修改示例状态 - - 参数: - - data (BatchSetAvailable): 批量修改示例状态模型 - - auth (AuthSchema): 认证信息模型 - - 返回: - - JSONResponse: 包含批量修改示例状态详情的JSON响应 - """ - await DemoService.set_available_service(auth=auth, data=data) + service = DemoService(auth) + await service.set_available(data=data) return SuccessResponse(msg="批量修改示例状态成功") @@ -174,18 +116,9 @@ async def export_obj_list_controller( search: Annotated[DemoQueryParam, Depends()], auth: Annotated[AuthSchema, Depends(AuthPermission(["module_example:demo:export"]))], ) -> StreamingResponse: - """ - 导出示例 - - 参数: - - search (DemoQueryParam): 查询参数 - - auth (AuthSchema): 认证信息模型 - - 返回: - - StreamingResponse: 包含示例列表的Excel文件流响应 - """ - result_dict_list = await DemoService.list_service(search=search, auth=auth) - export_result = await DemoService.batch_export_service(obj_list=result_dict_list) + service = DemoService(auth) + result_dict_list = await service.list(search=search) + export_result = DemoService.batch_export(obj_list=result_dict_list) return StreamResponse( data=bytes2file_response(export_result), @@ -203,18 +136,9 @@ async def import_obj_list_controller( file: UploadFile, auth: Annotated[AuthSchema, Depends(AuthPermission(["module_example:demo:import"]))], ) -> JSONResponse: - """ - 导入示例 - - 参数: - - file (UploadFile): 导入的Excel文件 - - auth (AuthSchema): 认证信息模型 - - 返回: - - JSONResponse: 包含导入示例详情的JSON响应 - """ - batch_import_result = await DemoService.batch_import_service( - file=file, auth=auth, update_support=True + service = DemoService(auth) + batch_import_result = await service.batch_import( + file=file, update_support=True ) return SuccessResponse(data=batch_import_result, msg="导入示例成功") @@ -225,13 +149,7 @@ async def import_obj_list_controller( dependencies=[Depends(AuthPermission(["module_example:demo:download"]))], ) async def export_obj_template_controller() -> StreamingResponse: - """ - 获取示例导入模板 - - 返回: - - StreamingResponse: 包含示例导入模板的Excel文件流响应 - """ - import_template_result = await DemoService.import_template_download_service() + import_template_result = DemoService.import_template_download() return StreamResponse( data=bytes2file_response(import_template_result), diff --git a/backend/app/plugin/module_example/demo/service.py b/backend/app/plugin/module_example/demo/service.py index e01f671a..c0873d9d 100644 --- a/backend/app/plugin/module_example/demo/service.py +++ b/backend/app/plugin/module_example/demo/service.py @@ -19,72 +19,34 @@ from .schema import ( class DemoService: - """ - 示例管理模块服务层 - """ + """示例管理模块服务层""" - @classmethod - async def detail_service(cls, auth: AuthSchema, id: int) -> DemoOutSchema: - """ - 详情 + def __init__(self, auth: AuthSchema) -> None: + self.auth = auth - 参数: - - auth (AuthSchema): 认证信息模型 - - id (int): 示例ID - - 返回: - - DemoOutSchema: 示例详情 - """ - obj = await DemoCRUD(auth).get(id=id) + async def detail(self, id: int) -> DemoOutSchema: + obj = await DemoCRUD(self.auth).get(id=id) if not obj: raise CustomException(msg="该数据不存在") return DemoOutSchema.model_validate(obj) - @classmethod - async def list_service( - cls, - auth: AuthSchema, + async def list( + self, search: DemoQueryParam | None = None, order_by: list[dict[str, str]] | None = None, ) -> list[DemoOutSchema]: - """ - 列表查询 - - 参数: - - auth (AuthSchema): 认证信息模型 - - search (DemoQueryParam | None): 查询参数 - - order_by (list[dict[str, str]] | None): 排序参数 - - 返回: - - list[DemoOutSchema]: 示例列表 - """ - obj_list = await DemoCRUD(auth).list(search=vars(search) if search else None, order_by=order_by) + obj_list = await DemoCRUD(self.auth).list(search=vars(search) if search else None, order_by=order_by) return [DemoOutSchema.model_validate(obj) for obj in obj_list] - @classmethod - async def page_service( - cls, - auth: AuthSchema, + async def page( + self, page_no: int, page_size: int, search: DemoQueryParam | None = None, order_by: list[dict[str, str]] | None = None, ) -> dict: - """ - 分页查询 - - 参数: - - auth (AuthSchema): 认证信息模型 - - page_no (int): 页码 - - page_size (int): 每页数量 - - search (DemoQueryParam | None): 查询参数 - - order_by (list[dict[str, str]] | None): 排序参数 - - 返回: - - dict: 分页数据 - """ offset = (page_no - 1) * page_size - return await DemoCRUD(auth).page( + return await DemoCRUD(self.auth).page( offset=offset, limit=page_size, order_by=order_by or [{"id": "asc"}], @@ -92,94 +54,40 @@ class DemoService: out_schema=DemoOutSchema, ) - @classmethod - async def create_service(cls, auth: AuthSchema, data: DemoCreateSchema) -> DemoOutSchema: - """ - 创建 - - 参数: - - auth (AuthSchema): 认证信息模型 - - data (DemoCreateSchema): 示例创建模型 - - 返回: - - DemoOutSchema: 新创建的示例 - """ - obj = await DemoCRUD(auth).get(name=data.name) + async def create(self, data: DemoCreateSchema) -> DemoOutSchema: + obj = await DemoCRUD(self.auth).get(name=data.name) if obj: raise CustomException(msg="创建失败,名称已存在") - obj = await DemoCRUD(auth).create(data=data) + obj = await DemoCRUD(self.auth).create(data=data) return DemoOutSchema.model_validate(obj) - @classmethod - async def update_service(cls, auth: AuthSchema, id: int, data: DemoUpdateSchema) -> DemoOutSchema: - """ - 更新 - - 参数: - - auth (AuthSchema): 认证信息模型 - - id (int): 示例ID - - data (DemoUpdateSchema): 示例更新模型 - - 返回: - - DemoOutSchema: 更新后的示例 - """ - obj = await DemoCRUD(auth).get(id=id) + async def update(self, id: int, data: DemoUpdateSchema) -> DemoOutSchema: + obj = await DemoCRUD(self.auth).get(id=id) if not obj: raise CustomException(msg="更新失败,该数据不存在") - exist_obj = await DemoCRUD(auth).get(name=data.name) + exist_obj = await DemoCRUD(self.auth).get(name=data.name) if exist_obj and exist_obj.id != id: raise CustomException(msg="更新失败,名称重复") - obj = await DemoCRUD(auth).update(id=id, data=data) + obj = await DemoCRUD(self.auth).update(id=id, data=data) return DemoOutSchema.model_validate(obj) - @classmethod - async def delete_service(cls, auth: AuthSchema, ids: list[int]) -> None: - """ - 删除 - - 参数: - - auth (AuthSchema): 认证信息模型 - - ids (list[int]): 示例ID列表 - - 返回: - - None - """ + async def delete(self, ids: list[int]) -> None: if len(ids) < 1: raise CustomException(msg="删除失败,删除对象不能为空") - objs = await DemoCRUD(auth).list(search={"id": ("in", ids)}) + objs = await DemoCRUD(self.auth).list(search={"id": ("in", ids)}) obj_map = {o.id: o for o in objs} for id_ in ids: if id_ not in obj_map: raise CustomException(msg="删除失败,该数据不存在") - await DemoCRUD(auth).delete(ids=ids) + await DemoCRUD(self.auth).delete(ids=ids) - @classmethod - async def set_available_service(cls, auth: AuthSchema, data: BatchSetAvailable) -> None: - """ - 批量设置状态 + async def set_available(self, data: BatchSetAvailable) -> None: + await DemoCRUD(self.auth).set(ids=data.ids, status=data.status) - 参数: - - auth (AuthSchema): 认证信息模型 - - data (BatchSetAvailable): 批量设置状态模型 - - 返回: - - None - """ - await DemoCRUD(auth).set(ids=data.ids, status=data.status) - - @classmethod - async def batch_export_service(cls, obj_list: list[dict[str, Any]]) -> bytes: - """ - 批量导出 - - 参数: - - obj_list (list[dict[str, Any]]): 示例模型实例字典列表 - - 返回: - - bytes: Excel文件字节流 - """ + @staticmethod + def batch_export(obj_list: list[dict[str, Any]]) -> bytes: mapping_dict = { "id": "编号", "name": "名称", @@ -201,22 +109,7 @@ class DemoService: return ExcelUtil.export_list2excel(list_data=data, mapping_dict=mapping_dict) - @classmethod - async def batch_import_service( - cls, auth: AuthSchema, file: UploadFile, update_support: bool = False - ) -> str: - """ - 批量导入 - - 参数: - - auth (AuthSchema): 认证信息模型 - - file (UploadFile): 上传的Excel文件 - - update_support (bool): 是否支持更新存在数据 - - 返回: - - str: 导入结果信息 - """ - + async def batch_import(self, file: UploadFile, update_support: bool = False) -> str: header_dict = {"名称": "name", "状态": "status", "描述": "description"} try: @@ -263,15 +156,15 @@ class DemoService: "description": str(row["description"]), } - exists_obj = await DemoCRUD(auth).get(name=data["name"]) + exists_obj = await DemoCRUD(self.auth).get(name=data["name"]) if exists_obj: if update_support: - await DemoCRUD(auth).update(id=exists_obj.id, data=data) + await DemoCRUD(self.auth).update(id=exists_obj.id, data=data) success_count += 1 else: error_msgs.append(f"第{count}行: 对象 {data['name']} 已存在") else: - await DemoCRUD(auth).create(data=data) + await DemoCRUD(self.auth).create(data=data) success_count += 1 except Exception as e: @@ -287,14 +180,8 @@ class DemoService: logger.error(f"批量导入用户失败: {e!s}") raise CustomException(msg=f"导入失败: {e!s}") - @classmethod - async def import_template_download_service(cls) -> bytes: - """ - 下载导入模板 - - 返回: - - bytes: Excel文件字节流 - """ + @staticmethod + def import_template_download() -> bytes: header_list = ["名称", "状态", "描述"] selector_header_list = ["状态"] option_list = [{"状态": ["正常", "停用"]}] diff --git a/backend/app/plugin/module_generator/gencode/controller.py b/backend/app/plugin/module_generator/gencode/controller.py index a2b83449..380faa77 100644 --- a/backend/app/plugin/module_generator/gencode/controller.py +++ b/backend/app/plugin/module_generator/gencode/controller.py @@ -21,7 +21,7 @@ from .schema import ( ) from .service import GenTableService -GenRouter = APIRouter(route_class=OperationLogRoute, prefix="/gencode", tags=["代码生成"]) +GenRouter = APIRouter(route_class=OperationLogRoute, prefix="/gencode", tags=["开发工具", "代码生成"]) @GenRouter.get( @@ -34,22 +34,10 @@ async def gen_table_list_controller( search: Annotated[GenTableQueryParam, Depends()], auth: Annotated[AuthSchema, Depends(AuthPermission(["module_generator:gencode:query"]))], ) -> JSONResponse: - """ - 查询代码生成业务表列表 - - 参数: - - page (PaginationQueryParam): 分页查询参数 - - search (GenTableQueryParam): 搜索参数 - - auth (AuthSchema): 认证信息模型 - - 返回: - - JSONResponse: 包含查询结果和分页信息的JSON响应 - """ order_by = [{"created_time": "desc"}] if page.order_by: order_by = page.order_by - result_dict = await GenTableService.get_gen_table_page_service( - auth=auth, + result_dict = await GenTableService(auth).get_gen_table_page( page_no=page.page_no, page_size=page.page_size, search=search, @@ -68,20 +56,7 @@ async def get_gen_db_table_list_controller( search: Annotated[GenTableQueryParam, Depends()], auth: Annotated[AuthSchema, Depends(AuthPermission(["module_generator:dblist:query"]))], ) -> JSONResponse: - """ - 查询数据库表列表 - - 参数: - - page (PaginationQueryParam): 分页查询参数 - - search (GenTableQueryParam): 搜索参数 - - auth (AuthSchema): 认证信息模型 - - 返回: - - JSONResponse: 包含查询结果和分页信息的JSON响应 - """ - # 优化:数据库侧分页(MySQL information_schema / Postgres pg_catalog),避免全量反射导致卡顿 - result_dict = await GenTableService.get_gen_db_table_page_service( - auth=auth, + result_dict = await GenTableService(auth).get_gen_db_table_page( page_no=page.page_no, page_size=page.page_size, search=search, @@ -96,23 +71,11 @@ async def get_gen_db_table_list_controller( ) async def import_gen_table_controller( table_names: Annotated[list[str], Body(description="表名列表")], - auth: Annotated[ - AuthSchema, - Depends(AuthPermission(["module_generator:gencode:import"])), - ], + auth: Annotated[AuthSchema, Depends(AuthPermission(["module_generator:gencode:import"]))], ) -> JSONResponse: - """ - 导入表结构 - - 参数: - - table_names (List[str]): 表名列表 - - auth (AuthSchema): 认证信息模型 - - 返回: - - JSONResponse: 包含导入结果和导入的表结构列表的JSON响应 - """ - add_gen_table_list = await GenTableService.get_gen_db_table_list_by_name_service(auth, table_names) - result = await GenTableService.import_gen_table_service(auth, add_gen_table_list) + svc = GenTableService(auth) + add_gen_table_list = await svc.get_gen_db_table_list_by_name(table_names) + result = await svc.import_gen_table(add_gen_table_list) return SuccessResponse(msg="导入表结构成功", data=result) @@ -125,18 +88,8 @@ async def gen_table_detail_controller( table_id: Annotated[int, Path(description="业务表ID")], auth: Annotated[AuthSchema, Depends(AuthPermission(["module_generator:gencode:query"]))], ) -> JSONResponse: - """ - 获取业务表详细信息 - - 参数: - - table_id (int): 业务表ID - - auth (AuthSchema): 认证信息模型 - - 返回: - - JSONResponse: 包含业务表详细信息的JSON响应 - """ - gen_table_detail_result = await GenTableService.get_gen_table_detail_service(auth, table_id) - return SuccessResponse(data=gen_table_detail_result, msg="获取业务表详细信息成功") + result = await GenTableService(auth).get_gen_table_detail(table_id) + return SuccessResponse(data=result, msg="获取业务表详细信息成功") @GenRouter.post( @@ -146,22 +99,9 @@ async def gen_table_detail_controller( ) async def create_table_controller( body: GenCreateTableSqlBody, - auth: Annotated[ - AuthSchema, - Depends(AuthPermission(["module_generator:gencode:create"])), - ], + auth: Annotated[AuthSchema, Depends(AuthPermission(["module_generator:gencode:create"]))], ) -> JSONResponse: - """ - 创建表结构 - - 参数: - - body (GenCreateTableSqlBody): 含 `sql` 字段的请求体(与前端 `data: { sql }` 一致) - - auth (AuthSchema): 认证信息模型 - - 返回: - - JSONResponse: 包含创建结果的JSON响应 - """ - result = await GenTableService.create_table_service(auth, body.sql) + result = await GenTableService(auth).create_table(body.sql) return SuccessResponse(msg="创建表结构成功", data=result) @@ -173,23 +113,9 @@ async def create_table_controller( async def update_gen_table_controller( table_id: Annotated[int, Path(description="业务表ID")], data: Annotated[GenTableSchema, Body(description="业务表信息")], - auth: Annotated[ - AuthSchema, - Depends(AuthPermission(["module_generator:gencode:update"])), - ], + auth: Annotated[AuthSchema, Depends(AuthPermission(["module_generator:gencode:update"]))], ) -> JSONResponse: - """ - 编辑业务表信息 - - 参数: - - table_id (int): 业务表ID - - data (GenTableSchema): 业务表信息模型 - - auth (AuthSchema): 认证信息模型 - - 返回: - - JSONResponse: 包含编辑结果的JSON响应 - """ - result_dict = await GenTableService.update_gen_table_service(auth, data, table_id) + result_dict = await GenTableService(auth).update_gen_table(data, table_id) return SuccessResponse(data=result_dict, msg="编辑业务表信息成功") @@ -200,22 +126,9 @@ async def update_gen_table_controller( ) async def delete_gen_table_controller( ids: Annotated[list[int], Body(description="业务表ID列表")], - auth: Annotated[ - AuthSchema, - Depends(AuthPermission(["module_generator:gencode:delete"])), - ], + auth: Annotated[AuthSchema, Depends(AuthPermission(["module_generator:gencode:delete"]))], ) -> JSONResponse: - """ - 删除业务表信息 - - 参数: - - ids (List[int]): 业务表ID列表 - - auth (AuthSchema): 认证信息模型 - - 返回: - - JSONResponse: 包含删除结果的JSON响应 - """ - result = await GenTableService.delete_gen_table_service(auth, ids) + result = await GenTableService(auth).delete_gen_table(ids) return SuccessResponse(msg="删除业务表信息成功", data=result) @@ -227,17 +140,7 @@ async def batch_gen_code_controller( table_names: Annotated[list[str], Body(description="表名列表")], auth: Annotated[AuthSchema, Depends(AuthPermission(["module_generator:gencode:operate"]))], ) -> StreamResponse: - """ - 批量生成代码 - - 参数: - - table_names (List[str]): 表名列表 - - auth (AuthSchema): 认证信息模型 - - 返回: - - StreamResponse: 包含批量生成代码的ZIP文件流响应 - """ - batch_gen_code_result, failed_tables = await GenTableService.batch_gen_code_service(auth, table_names) + batch_gen_code_result, failed_tables = await GenTableService(auth).batch_gen_code(table_names) headers = {"Content-Disposition": "attachment; filename=code.zip"} if failed_tables: logger.warning(f"批量生成代码部分失败,跳过表: {failed_tables}") @@ -258,17 +161,7 @@ async def gen_code_local_controller( table_name: Annotated[str, Path(description="表名")], auth: Annotated[AuthSchema, Depends(AuthPermission(["module_generator:gencode:code"]))], ) -> JSONResponse: - """ - 生成代码到指定路径 - - 参数: - - table_name (str): 表名 - - auth (AuthSchema): 认证信息模型 - - 返回: - - JSONResponse: 包含生成结果的JSON响应 - """ - result = await GenTableService.generate_code_service(auth, table_name) + result = await GenTableService(auth).generate_code(table_name) return SuccessResponse(msg="生成代码到指定路径成功", data=result) @@ -281,18 +174,8 @@ async def preview_code_controller( table_id: Annotated[int, Path(description="业务表ID")], auth: Annotated[AuthSchema, Depends(AuthPermission(["module_generator:gencode:query"]))], ) -> JSONResponse: - """ - 预览代码 - - 参数: - - table_id (int): 业务表ID - - auth (AuthSchema): 认证信息模型 - - 返回: - - JSONResponse: 包含预览代码的JSON响应 - """ - preview_code_result = await GenTableService.preview_code_service(auth, table_id) - return SuccessResponse(data=preview_code_result, msg="预览代码成功") + result = await GenTableService(auth).preview_code(table_id) + return SuccessResponse(data=result, msg="预览代码成功") @GenRouter.post( @@ -304,17 +187,7 @@ async def sync_db_controller( table_name: Annotated[str, Path(description="表名")], auth: Annotated[AuthSchema, Depends(AuthPermission(["module_generator:db:sync"]))], ) -> JSONResponse: - """ - 同步数据库 - - 参数: - - table_name (str): 表名 - - auth (AuthSchema): 认证信息模型 - - 返回: - - JSONResponse: 包含同步数据库结果的JSON响应 - """ - result = await GenTableService.sync_db_service(auth, table_name) + result = await GenTableService(auth).sync_db(table_name) return SuccessResponse(msg="同步数据库成功", data=result) @@ -327,15 +200,5 @@ async def sync_db_preview_controller( table_name: Annotated[str, Path(description="表名")], auth: Annotated[AuthSchema, Depends(AuthPermission(["module_generator:db:sync"]))], ) -> JSONResponse: - """ - 同步数据库前差异预览(主表 + 可选子表),不落库。 - - 参数: - - table_name (str): 物理表名。 - - auth (AuthSchema): 认证信息。 - - 返回: - - JSONResponse: 成功响应,data 为预览结构。 - """ - result = await GenTableService.sync_db_preview_service(auth, table_name) + result = await GenTableService(auth).sync_db_preview(table_name) return SuccessResponse(msg="获取同步差异预览成功", data=result) diff --git a/backend/app/plugin/module_generator/gencode/service.py b/backend/app/plugin/module_generator/gencode/service.py index 92c8c0a0..9763c971 100644 --- a/backend/app/plugin/module_generator/gencode/service.py +++ b/backend/app/plugin/module_generator/gencode/service.py @@ -70,8 +70,11 @@ _MENU_TYPE_MENU = 2 class GenTableService: """代码生成业务表服务层""" - @classmethod - async def _effective_package_name(cls, auth: AuthSchema, parent_catalog_id: int | None, package_name: str | None) -> str: + def __init__(self, auth: AuthSchema) -> None: + self.auth = auth + + + async def _effective_package_name(self, parent_catalog_id: int | None, package_name: str | None) -> str: """根据「是否选择上级目录」计算最终包名(分系统根目录)。 规则(与你描述一致): @@ -83,7 +86,7 @@ class GenTableService: if parent_catalog_id is not None: from app.api.v1.module_platform.menu.crud import MenuCRUD - m = await MenuCRUD(auth).get(id=parent_catalog_id) + m = await MenuCRUD(self.auth).get(id=parent_catalog_id) if not m: raise CustomException(msg="上级菜单不存在") route_path = (getattr(m, "route_path", None) or "").strip() @@ -102,20 +105,19 @@ class GenTableService: raise CustomException(msg="包名不能为空") return pn if pn.startswith("module_") else f"module_{pn}" - @classmethod - async def _assert_parent_menu_is_catalog(cls, auth: AuthSchema, parent_menu_id: int | None) -> None: + async def _assert_parent_menu_is_catalog(self, parent_menu_id: int | None) -> None: """上级菜单仅允许目录:与前端树只展示目录一致,避免挂到菜单/按钮下。""" if parent_menu_id is None: return from app.api.v1.module_platform.menu.crud import MenuCRUD - m = await MenuCRUD(auth).get(id=parent_menu_id) + m = await MenuCRUD(self.auth).get(id=parent_menu_id) if not m: raise CustomException(msg="上级菜单不存在") if m.type != _MENU_TYPE_CATALOG: raise CustomException(msg="上级菜单须选择目录类型") - @classmethod + @staticmethod def _menu_route_first_segment(cls, parent_catalog_id: int | None, package_name: str, module_name: str | None) -> str: """前端页面路由首段(与菜单 ``route_path`` 第一段一致)。 @@ -128,7 +130,6 @@ class GenTableService: raise CustomException(msg="包名不能为空") return pn if pn.startswith("module_") else f"module_{pn}" - @classmethod def _catalog_menu_dir_key(cls, parent_catalog_id: int | None, package_name: str, module_name: str | None) -> str: """菜单上「模块目录」节点的 name(与路由第一段 package 独立)。 @@ -144,9 +145,8 @@ class GenTableService: raise CustomException(msg="模块名不能为空") return mn - @classmethod async def _get_or_create_package_directory_menu( - cls, + self, menu_crud: Any, parent_catalog_id: int | None, package_name: str, @@ -161,7 +161,7 @@ class GenTableService: if not pn: raise CustomException(msg="包名不能为空") mn = (module_name or "").strip() - dir_key = cls._catalog_menu_dir_key(parent_catalog_id, pn, module_name) + dir_key = self._catalog_menu_dir_key(parent_catalog_id, pn, module_name) if parent_catalog_id is not None: existing = await menu_crud.get(name=dir_key, type=_MENU_TYPE_CATALOG, parent_id=parent_catalog_id) @@ -171,7 +171,7 @@ class GenTableService: logger.info(f"代码生成:复用模块目录菜单 id={existing.id} name={dir_key!r} parent={parent_catalog_id!r}") return int(existing.id) - route_first = cls._menu_route_first_segment(parent_catalog_id, pn, module_name) + route_first = self._menu_route_first_segment(parent_catalog_id, pn, module_name) # 目录菜单固定跳到模块根:/{module_xxx}/{module_name} catalog_route_path = f"/{route_first}/{mn}" redirect = f"/{route_first}/{mn}" @@ -202,7 +202,7 @@ class GenTableService: logger.info(f"代码生成:新建模块目录菜单 id={created.id} name={dir_key!r} under_parent={parent_catalog_id!r}") return int(created.id) - @classmethod + @staticmethod def normalize_and_validate_master_sub(cls, data: GenTableSchema) -> None: """ 主子表业务规则:子表表名与外键列同填或同空;子表表名不得与主表相同。 @@ -224,9 +224,8 @@ class GenTableService: if sn and fk and sn == tn: raise CustomException(msg="子表表名不能与主表表名相同") - @classmethod @handle_service_exception - async def get_gen_table_detail_service(cls, auth: AuthSchema, table_id: int) -> GenTableOutSchema: + async def get_gen_table_detail(self, table_id: int) -> GenTableOutSchema: """获取详细信息。 参数: @@ -236,12 +235,11 @@ class GenTableService: 返回: - dict: 包含业务表详细信息的字典。 """ - gen_table = await cls.get_gen_table_by_id_service(auth, table_id) + gen_table = await self.get_gen_table_by_id(table_id) return gen_table - @classmethod @handle_service_exception - async def get_gen_table_list_service(cls, auth: AuthSchema, search: GenTableQueryParam) -> list[dict]: + async def get_gen_table_list(self, search: GenTableQueryParam) -> list[dict]: """ 获取代码生成业务表列表信息。 @@ -252,14 +250,12 @@ class GenTableService: 返回: - list[dict]: 包含业务表列表信息的字典列表。 """ - gen_table_list_result = await GenTableCRUD(auth=auth).get_gen_table_list(search) + gen_table_list_result = await GenTableCRUD(auth=self.auth).get_gen_table_list(search) return [GenTableOutSchema.model_validate(obj) for obj in gen_table_list_result] - @classmethod @handle_service_exception - async def get_gen_table_page_service( - cls, - auth: AuthSchema, + async def get_gen_table_page( + self, page_no: int, page_size: int, search: GenTableQueryParam, @@ -280,7 +276,7 @@ class GenTableService: """ offset = (page_no - 1) * page_size order = order_by or [{"created_time": "desc"}] - return await GenTableCRUD(auth=auth).page( + return await GenTableCRUD(auth=self.auth).page( offset=offset, limit=page_size, order_by=order, @@ -288,9 +284,8 @@ class GenTableService: out_schema=GenTableOutSchema, ) - @classmethod @handle_service_exception - async def get_gen_db_table_list_service(cls, auth: AuthSchema, search: GenTableQueryParam) -> list[Any]: + async def get_gen_db_table_list(self, search: GenTableQueryParam) -> list[Any]: """获取数据库表列表。 参数: @@ -300,14 +295,12 @@ class GenTableService: 返回: - list[Any]: 包含数据库表列表信息的任意类型列表。 """ - gen_db_table_list_result = await GenTableCRUD(auth=auth).get_db_table_list(search) + gen_db_table_list_result = await GenTableCRUD(auth=self.auth).get_db_table_list(search) return gen_db_table_list_result - @classmethod @handle_service_exception - async def get_gen_db_table_page_service( - cls, - auth: AuthSchema, + async def get_gen_db_table_page( + self, page_no: int, page_size: int, search: GenTableQueryParam, @@ -325,7 +318,7 @@ class GenTableService: - dict[str, Any]: 含 items、total、has_next 等字段。 """ offset = (page_no - 1) * page_size - items, total = await GenTableCRUD(auth=auth).get_db_table_page(search=search, offset=offset, limit=page_size) + items, total = await GenTableCRUD(auth=self.auth).get_db_table_page(search=search, offset=offset, limit=page_size) return { "items": items, "total": total, @@ -334,9 +327,8 @@ class GenTableService: "has_next": offset + page_size < total, } - @classmethod @handle_service_exception - async def get_gen_db_table_list_by_name_service(cls, auth: AuthSchema, table_names: list[str]) -> list[GenTableOutSchema]: + async def get_gen_db_table_list_by_name(self, table_names: list[str]) -> list[GenTableOutSchema]: """根据表名称组获取数据库表信息。 参数: @@ -346,16 +338,15 @@ class GenTableService: 返回: - list[GenTableOutSchema]: 包含业务表详细信息的模型列表。 """ - gen_db_table_list_result = await GenTableCRUD(auth).get_db_table_list_by_names(table_names) + gen_db_table_list_result = await GenTableCRUD(auth=self.auth).get_db_table_list_by_names(table_names) # 修复:将GenDBTableSchema对象转换为字典后再传递给GenTableOutSchema result = [GenTableOutSchema(**gen_table.model_dump()) for gen_table in gen_db_table_list_result] return result - @classmethod @handle_service_exception - async def import_gen_table_service(cls, auth: AuthSchema, gen_table_list: list[GenTableOutSchema]) -> bool: + async def import_gen_table(self, gen_table_list: list[GenTableOutSchema]) -> bool: """导入表结构到生成器。 参数: @@ -371,17 +362,17 @@ class GenTableService: try: for table in gen_table_list: _row = {k: v for k, v in table.model_dump().items() if k in GenTableSchema.model_fields} - cls.normalize_and_validate_master_sub(GenTableSchema.model_validate(_row)) + self.normalize_and_validate_master_sub(GenTableSchema.model_validate(_row)) table_name = table.table_name # 检查表是否已存在 - existing_table = await GenTableCRUD(auth).get_gen_table_by_name(table_name) + existing_table = await GenTableCRUD(auth=self.auth).get_gen_table_by_name(table_name) if existing_table: raise CustomException(msg=f"以下表已存在,不能重复导入: {table_name}") GenUtils.init_table(table) if not table.columns: table.columns = [] - add_gen_table = await GenTableCRUD(auth).add_gen_table(GenTableSchema.model_validate(table.model_dump())) - gen_table_columns = await GenTableColumnCRUD(auth).get_gen_db_table_columns_by_name(table_name) + add_gen_table = await GenTableCRUD(auth=self.auth).add_gen_table(GenTableSchema.model_validate(table.model_dump())) + gen_table_columns = await GenTableColumnCRUD(auth=self.auth).get_gen_db_table_columns_by_name(table_name) if len(gen_table_columns) > 0: table.id = add_gen_table.id for column in gen_table_columns: @@ -401,14 +392,13 @@ class GenTableService: python_field=column.python_field, ) GenUtils.init_column_field(column_schema, table) - await GenTableColumnCRUD(auth).create_gen_table_column_crud(column_schema) + await GenTableColumnCRUD(auth=self.auth).create_gen_table_column_crud(column_schema) return True except Exception as e: raise CustomException(msg=f"导入失败, {e!s}") - @classmethod @handle_service_exception - async def create_table_service(cls, auth: AuthSchema, sql: str) -> bool | None: + async def create_table(self, sql: str) -> bool | None: """创建表结构并导入至代码生成模块。 参数: @@ -448,7 +438,7 @@ class GenTableService: table_names = list(set(table_names)) # 创建CRUD实例 - gen_table_crud = GenTableCRUD(auth=auth) + gen_table_crud = GenTableCRUD(auth=self.auth) # 检查每个表是否已存在 for table_name in table_names: @@ -480,18 +470,17 @@ class GenTableService: # 建表成功后自动导入到代码生成模块 if table_names: - gen_table_list = await cls.get_gen_db_table_list_by_name_service(auth, table_names) + gen_table_list = await self.get_gen_db_table_list_by_name(table_names) if gen_table_list: - await cls.import_gen_table_service(auth, gen_table_list) + await self.import_gen_table(gen_table_list) return True except Exception as e: raise CustomException(msg=f"创建表结构失败: {e!s}") - @classmethod @handle_service_exception - async def update_gen_table_service(cls, auth: AuthSchema, data: GenTableSchema, table_id: int) -> GenTableOutSchema: + async def update_gen_table(self, data: GenTableSchema, table_id: int) -> GenTableOutSchema: """编辑业务表信息。 参数: @@ -503,18 +492,18 @@ class GenTableService: - dict[str, Any]: 更新后的业务表信息。 """ # 处理params为None的情况 - gen_table_info = await cls.get_gen_table_by_id_service(auth, table_id) + gen_table_info = await self.get_gen_table_by_id(table_id) if gen_table_info.id: try: - cls.normalize_and_validate_master_sub(data) - await cls._assert_parent_menu_is_catalog(auth, data.parent_menu_id) + self.normalize_and_validate_master_sub(data) + await self._assert_parent_menu_is_catalog(data.parent_menu_id) # 直接调用edit_gen_table方法,它会在内部处理排除嵌套字段的逻辑 - result = await GenTableCRUD(auth).edit_gen_table(table_id, data) + result = await GenTableCRUD(auth=self.auth).edit_gen_table(table_id, data) if not result: raise CustomException(msg="更新业务表信息失败") if data.columns is not None: - db_columns = await GenTableColumnCRUD(auth).list_gen_table_column_crud(search={"table_id": table_id}) + db_columns = await GenTableColumnCRUD(auth=self.auth).list_gen_table_column_crud(search={"table_id": table_id}) db_column_map = {c.column_name: c for c in db_columns if c.column_name} submitted_names = {c.column_name for c in data.columns if hasattr(c, "column_name") and c.column_name} @@ -525,7 +514,7 @@ class GenTableService: # 只更新前端实际修改的字段(利用 Pydantic model_fields_set) update_data = gen_table_column.model_dump(exclude_unset=True, exclude={"id", "super_column"}) if update_data: - await GenTableColumnCRUD(auth).update(id=col_id, data=update_data) + await GenTableColumnCRUD(auth=self.auth).update(id=col_id, data=update_data) else: # 新增列:前端新增但库中无对应记录 column_schema = GenTableColumnSchema( @@ -533,19 +522,19 @@ class GenTableService: **gen_table_column.model_dump(exclude={"id", "super_column"}), ) GenUtils.init_column_field(column_schema, gen_table_info) - await GenTableColumnCRUD(auth).create_gen_table_column_crud(column_schema) + await GenTableColumnCRUD(auth=self.auth).create_gen_table_column_crud(column_schema) # 删除前端已移除的列 for db_name, db_col in db_column_map.items(): if db_name not in submitted_names: db_id = getattr(db_col, "id", None) if db_id: - await GenTableColumnCRUD(auth).delete(ids=[db_id]) + await GenTableColumnCRUD(auth=self.auth).delete(ids=[db_id]) # 重新获取带有预加载关系的对象,避免懒加载导致的MissingGreenlet错误 - updated_gen_table = await GenTableCRUD(auth).get_gen_table_by_id(table_id) + updated_gen_table = await GenTableCRUD(auth=self.auth).get_gen_table_by_id(table_id) out = GenTableOutSchema.model_validate(updated_gen_table) - await cls.set_pk_column(out) - await cls.hydrate_sub_table(auth, out) + await self.set_pk_column(out) + await self.hydrate_sub_table(out) return out except CustomException: raise @@ -554,9 +543,8 @@ class GenTableService: else: raise CustomException(msg="业务表不存在") - @classmethod @handle_service_exception - async def delete_gen_table_service(cls, auth: AuthSchema, ids: list[int]) -> None: + async def delete_gen_table(self, ids: list[int]) -> None: """删除业务表信息(先删字段,再删表)。 参数: @@ -572,15 +560,14 @@ class GenTableService: try: # 先删除相关的字段信息 - await GenTableColumnCRUD(auth=auth).delete_gen_table_column_by_table_id_crud(ids) + await GenTableColumnCRUD(auth=self.auth).delete_gen_table_column_by_table_id_crud(ids) # 再删除表信息 - await GenTableCRUD(auth=auth).delete_gen_table(ids) + await GenTableCRUD(auth=self.auth).delete_gen_table(ids) except Exception as e: raise CustomException(msg=str(e)) - @classmethod @handle_service_exception - async def get_gen_table_by_id_service(cls, auth: AuthSchema, table_id: int) -> GenTableOutSchema: + async def get_gen_table_by_id(self, table_id: int) -> GenTableOutSchema: """获取需要生成代码的业务表详细信息。 参数: @@ -590,18 +577,17 @@ class GenTableService: 返回: - GenTableOutSchema: 业务表详细信息模型。 """ - gen_table = await GenTableCRUD(auth=auth).get_gen_table_by_id(table_id) + gen_table = await GenTableCRUD(auth=self.auth).get_gen_table_by_id(table_id) if not gen_table: raise CustomException(msg="业务表不存在") result = GenTableOutSchema.model_validate(gen_table) - await cls.set_pk_column(result) - await cls.hydrate_sub_table(auth, result) + await self.set_pk_column(result) + await self.hydrate_sub_table(result) return result - @classmethod @handle_service_exception - async def get_gen_table_all_service(cls, auth: AuthSchema) -> list[GenTableOutSchema]: + async def get_gen_table_all(self) -> list[GenTableOutSchema]: """获取所有业务表信息(列表)。 参数: @@ -610,7 +596,7 @@ class GenTableService: 返回: - list[GenTableOutSchema]: 业务表详细信息模型列表。 """ - gen_table_all = await GenTableCRUD(auth=auth).get_gen_table_all() or [] + gen_table_all = await GenTableCRUD(auth=self.auth).get_gen_table_all() or [] result = [] for gen_table in gen_table_all: try: @@ -621,9 +607,8 @@ class GenTableService: continue return result - @classmethod @handle_service_exception - async def preview_code_service(cls, auth: AuthSchema, table_id: int) -> dict[str, Any]: + async def preview_code(self, table_id: int) -> dict[str, Any]: """ 预览代码(根据模板渲染内存结果)。 @@ -634,17 +619,17 @@ class GenTableService: 返回: - dict[str, Any]: 文件名到渲染内容的映射。 """ - raw = await GenTableCRUD(auth).get_gen_table_by_id(table_id) + raw = await GenTableCRUD(auth=self.auth).get_gen_table_by_id(table_id) if not raw: raise CustomException(msg="业务表不存在") gen_table = GenTableOutSchema.model_validate(raw) - await cls.set_pk_column(gen_table) - await cls.hydrate_sub_table(auth, gen_table) - cls._assert_master_sub_config_valid(gen_table) + await self.set_pk_column(gen_table) + await self.hydrate_sub_table(gen_table) + self._assert_master_sub_config_valid(gen_table) # 预览回显的路径/包名规则必须与「写入本地」一致: # - 选择上级目录:继承上级目录所属 module_xxx # - 未选上级目录:使用表单包名(并补齐 module_ 前缀) - gen_table.package_name = await cls._effective_package_name(auth, gen_table.parent_menu_id, gen_table.package_name) + gen_table.package_name = await self._effective_package_name(gen_table.parent_menu_id, gen_table.package_name) # 子表与主表同分系统/同模块 if gen_table.sub and gen_table.sub_table: gen_table.sub_table.package_name = gen_table.package_name @@ -678,9 +663,8 @@ class GenTableService: preview_code_result[out_key] = f"渲染错误: {e!s}" return preview_code_result - @classmethod @handle_service_exception - async def generate_code_service(cls, auth: AuthSchema, table_name: str) -> bool: + async def generate_code(self, table_name: str) -> bool: """生成代码至指定路径(安全写入+可跳过覆盖)。 菜单固定为 **目录(type=1) + 菜单(type=2) + 按钮(type=3)**: @@ -703,7 +687,7 @@ class GenTableService: if not table_name or not table_name.strip(): raise CustomException(msg="表名不能为空") env = Jinja2TemplateUtil.get_env() - render_info = await cls.__get_gen_render_info(auth, table_name) + render_info = await self.__get_gen_render_info(table_name) gen_table_schema: GenTableOutSchema = render_info[3] from app.api.v1.module_platform.menu.crud import MenuCRUD @@ -711,7 +695,7 @@ class GenTableService: from app.utils.common_util import CamelCaseUtil # 按“上级目录”规则矫正最终包名(分系统根) - gen_table_schema.package_name = await cls._effective_package_name(auth, gen_table_schema.parent_menu_id, gen_table_schema.package_name) + gen_table_schema.package_name = await self._effective_package_name(gen_table_schema.parent_menu_id, gen_table_schema.package_name) # 统一权限前缀(对齐 module_example/demo): # - module_xxx:module_name(操作在按钮/模板中追加 :query/:create...) pn = (gen_table_schema.package_name or "").strip() @@ -758,10 +742,10 @@ class GenTableService: await _write_templates(sub_templates, sub_ctx, gen_table_schema.sub_table) # 2. 代码成功写入后,再创建菜单(避免失败时产生孤儿菜单数据) - menu_crud = MenuCRUD(auth) - await cls._assert_parent_menu_is_catalog(auth, gen_table_schema.parent_menu_id) + menu_crud = MenuCRUD(self.auth) + await self._assert_parent_menu_is_catalog(gen_table_schema.parent_menu_id) # 1. 目录 + 菜单 + 按钮:先取/建模块目录(名称规则见 _catalog_menu_dir_key) - dir_menu_id = await cls._get_or_create_package_directory_menu( + dir_menu_id = await self._get_or_create_package_directory_menu( menu_crud, gen_table_schema.parent_menu_id, gen_table_schema.package_name, @@ -777,7 +761,7 @@ class GenTableService: ) if existing_func_menu: raise CustomException(msg=f"该模块目录下功能菜单「{gen_table_schema.function_name}」已存在,不能重复创建") - route_seg = cls._menu_route_first_segment( + route_seg = self._menu_route_first_segment( gen_table_schema.parent_menu_id, gen_table_schema.package_name or "", gen_table_schema.module_name, @@ -897,9 +881,8 @@ class GenTableService: return True - @classmethod @handle_service_exception - async def batch_gen_code_service(cls, auth: AuthSchema, table_names: list[str]) -> tuple[bytes, list[str]]: + async def batch_gen_code(self, table_names: list[str]) -> tuple[bytes, list[str]]: """ 批量生成代码并打包为ZIP。 - 备注:内存生成并压缩,兼容多模板类型;供下载使用。 @@ -921,7 +904,7 @@ class GenTableService: for table_name in valid_names: try: env = Jinja2TemplateUtil.get_env() - render_info = await cls.__get_gen_render_info(auth, table_name) + render_info = await self.__get_gen_render_info(table_name) gen_tbl = render_info[3] for template_file, output_file in zip(render_info[0], render_info[1], strict=False): render_content = await env.get_template(template_file).render_async(**render_info[2]) @@ -947,9 +930,8 @@ class GenTableService: raise CustomException(msg="未能生成任何代码文件:请检查所选表是否存在于代码生成配置中,或主子表、字段配置是否正确") return zip_data, failed_tables - @classmethod @handle_service_exception - async def sync_db_service(cls, auth: AuthSchema, table_name: str, _sync_sub: bool = True) -> None: + async def sync_db(self, table_name: str, _sync_sub: bool = True) -> None: """ 同步数据库表结构到业务表。 @@ -963,7 +945,7 @@ class GenTableService: # 验证表名非空 if not table_name or not table_name.strip(): raise CustomException(msg="表名不能为空") - gen_table = await GenTableCRUD(auth).get_gen_table_by_name(table_name) + gen_table = await GenTableCRUD(auth=self.auth).get_gen_table_by_name(table_name) if not gen_table: raise CustomException(msg="业务表不存在") table = GenTableOutSchema.model_validate(gen_table) @@ -972,7 +954,7 @@ class GenTableService: table_columns = table.columns or [] table_column_map = {column.column_name: column for column in table_columns} # 确保db_table_columns始终是列表类型,避免None值 - db_table_columns = await GenTableColumnCRUD(auth).get_gen_db_table_columns_by_name(table_name) or [] + db_table_columns = await GenTableColumnCRUD(auth=self.auth).get_gen_db_table_columns_by_name(table_name) or [] db_table_columns = [col for col in db_table_columns if col is not None] db_table_column_names = [column.column_name for column in db_table_columns] try: @@ -1017,33 +999,32 @@ class GenTableService: # 转换为 GenTableColumnSchema,排除 super_column 等输出专用字段 column_data = GenTableColumnSchema(**column.model_dump(exclude={"super_column"})) if hasattr(column, "id") and column.id: - await GenTableColumnCRUD(auth).update_gen_table_column_crud(column.id, column_data) + await GenTableColumnCRUD(auth=self.auth).update_gen_table_column_crud(column.id, column_data) else: - await GenTableColumnCRUD(auth).create_gen_table_column_crud(column_data) + await GenTableColumnCRUD(auth=self.auth).create_gen_table_column_crud(column_data) else: # 设置table_id以确保新字段能正确关联到表 column.table_id = table.id # 转换为 GenTableColumnSchema,排除 super_column 等输出专用字段 column_data = GenTableColumnSchema(**column.model_dump(exclude={"super_column"})) - await GenTableColumnCRUD(auth).create_gen_table_column_crud(column_data) + await GenTableColumnCRUD(auth=self.auth).create_gen_table_column_crud(column_data) del_columns = [column for column in table_columns if column.column_name not in db_table_column_names] if del_columns: for column in del_columns: if hasattr(column, "id") and column.id: - await GenTableColumnCRUD(auth).delete_gen_table_column_by_column_id_crud([column.id]) + await GenTableColumnCRUD(auth=self.auth).delete_gen_table_column_by_column_id_crud([column.id]) # 主子表:若子表也已导入生成器,则一并同步子表配置 sn = (table.sub_table_name or "").strip() fk = (table.sub_table_fk_name or "").strip() if _sync_sub and sn and fk: - sub_cfg = await GenTableCRUD(auth).get_gen_table_by_name(sn) + sub_cfg = await GenTableCRUD(auth=self.auth).get_gen_table_by_name(sn) if sub_cfg: - await cls.sync_db_service(auth, sn, _sync_sub=False) + await self.sync_db(sn, _sync_sub=False) except Exception as e: raise CustomException(msg=f"同步失败: {e!s}") - @classmethod - async def hydrate_sub_table(cls, auth: AuthSchema, gen_table: GenTableOutSchema) -> None: + async def hydrate_sub_table(self, gen_table: GenTableOutSchema) -> None: """ 主子表:优先使用已导入的子表配置,否则回退为只读 DB 结构。 @@ -1081,12 +1062,12 @@ class GenTableService: # 1) 若子表已作为 gen_table 导入,则使用其 columns 配置(可控、可复用) try: - sub_cfg_model = await GenTableCRUD(auth).get_gen_table_by_name(sub_name_raw, preload=["columns"]) + sub_cfg_model = await GenTableCRUD(auth=self.auth).get_gen_table_by_name(sub_name_raw, preload=["columns"]) except Exception: sub_cfg_model = None if sub_cfg_model: sub_cfg = GenTableOutSchema.model_validate(sub_cfg_model) - await cls.set_pk_column(sub_cfg) + await self.set_pk_column(sub_cfg) # 校验外键列存在于子表配置中 fk_names = {c.column_name for c in (sub_cfg.columns or []) if c.column_name} if fk_raw not in fk_names: @@ -1101,7 +1082,7 @@ class GenTableService: # 2) 回退:仅从 DB 读取结构(只读,无法配置子表字段) try: - gen_table_columns = await GenTableColumnCRUD(auth).get_gen_db_table_columns_by_name(sub_name_raw) + gen_table_columns = await GenTableColumnCRUD(auth=self.auth).get_gen_db_table_columns_by_name(sub_name_raw) except Exception as e: logger.warning(f"获取子表 {sub_name_raw} 字段失败: {e!s}") gen_table.sub = False @@ -1119,7 +1100,7 @@ class GenTableService: gen_table.sub_table = None gen_table.master_sub_hint = f"子表「{sub_name_raw}」中不存在名为「{fk_raw}」的列,请核对外键列名" return - table_comment = await GenTableCRUD(auth).get_db_table_comment(sub_name_raw) + table_comment = await GenTableCRUD(auth=self.auth).get_db_table_comment(sub_name_raw) sub = GenTableOutSchema.model_validate( { "id": -1, @@ -1146,14 +1127,14 @@ class GenTableService: if sub.columns is None: sub.columns = [] sub.columns.append(GenTableColumnOutSchema(**col_schema.model_dump())) - await cls.set_pk_column(sub) + await self.set_pk_column(sub) gen_table.sub = True gen_table.sub_table = sub gen_table.master_sub_hint = f"主子表已启用:当前子表仅从数据库结构读取(只读)。若想可配置子表字段,请先在「导入」中把子表「{sub_name_raw}」也导入生成器。" - @classmethod + @staticmethod def _sync_preview_diff( - cls, + self, current_cols: list[GenTableColumnOutSchema], db_cols: list[GenTableColumnOutSchema], ) -> tuple[list[str], list[str], list[GenSyncColumnChange], int]: @@ -1201,9 +1182,8 @@ class GenTableService: return added, removed, changed, unchanged - @classmethod @handle_service_exception - async def sync_db_preview_service(cls, auth: AuthSchema, table_name: str) -> GenSyncPreviewSchema: + async def sync_db_preview(self, table_name: str) -> GenSyncPreviewSchema: """ 同步数据库前差异预览(主表 + 可选子表)。 @@ -1219,7 +1199,7 @@ class GenTableService: """ if not table_name or not table_name.strip(): raise CustomException(msg="表名不能为空") - gen_table = await GenTableCRUD(auth).get_gen_table_by_name(table_name, preload=["columns"]) + gen_table = await GenTableCRUD(auth=self.auth).get_gen_table_by_name(table_name, preload=["columns"]) if not gen_table: raise CustomException(msg="业务表不存在") @@ -1227,8 +1207,8 @@ class GenTableService: if not table.id: raise CustomException(msg="业务表ID不能为空") - db_cols = await GenTableColumnCRUD(auth).get_gen_db_table_columns_by_name(table_name) - added, removed, changed, unchanged = cls._sync_preview_diff( + db_cols = await GenTableColumnCRUD(auth=self.auth).get_gen_db_table_columns_by_name(table_name) + added, removed, changed, unchanged = self._sync_preview_diff( current_cols=table.columns or [], db_cols=db_cols or [], ) @@ -1246,13 +1226,13 @@ class GenTableService: if sn and fk: preview.sub_table_name = sn # 优先取“已导入的子表配置”,否则用 DB 结构(只读) - sub_cfg = await GenTableCRUD(auth).get_gen_table_by_name(sn, preload=["columns"]) + sub_cfg = await GenTableCRUD(auth=self.auth).get_gen_table_by_name(sn, preload=["columns"]) if sub_cfg: cur_sub_cols = GenTableOutSchema.model_validate(sub_cfg).columns or [] else: cur_sub_cols = [] - db_sub_cols = await GenTableColumnCRUD(auth).get_gen_db_table_columns_by_name(sn) - s_added, s_removed, s_changed, s_unchanged = cls._sync_preview_diff( + db_sub_cols = await GenTableColumnCRUD(auth=self.auth).get_gen_db_table_columns_by_name(sn) + s_added, s_removed, s_changed, s_unchanged = self._sync_preview_diff( current_cols=cur_sub_cols, db_cols=db_sub_cols or [], ) @@ -1266,7 +1246,7 @@ class GenTableService: return preview - @classmethod + @staticmethod def _assert_master_sub_config_valid(cls, gen_table: GenTableOutSchema) -> None: """预览/生成前校验主子表配置是否可用。""" sn = (gen_table.sub_table_name or "").strip() @@ -1278,7 +1258,7 @@ class GenTableService: if not gen_table.sub_table: raise CustomException(msg=gen_table.master_sub_hint or "无法生成主子表代码:请确认子表已在当前数据库中存在,且外键列名正确") - @classmethod + @staticmethod async def set_pk_column(cls, gen_table: GenTableOutSchema) -> None: """设置主键列信息(主表/子表)。 - 备注:同时兼容`pk`布尔与`is_pk == '1'`字符串两种标识。 @@ -1299,8 +1279,7 @@ class GenTableService: if gen_table.pk_column is None and gen_table.columns: gen_table.pk_column = gen_table.columns[0] - @classmethod - async def __get_gen_render_info(cls, auth: AuthSchema, table_name: str) -> list[Any]: + async def __get_gen_render_info(self, table_name: str) -> list[Any]: """ 获取生成代码渲染模板相关信息。 @@ -1314,16 +1293,16 @@ class GenTableService: 异常: - CustomException: 当业务表不存在或数据转换失败时抛出。 """ - gen_table_model = await GenTableCRUD(auth=auth).get_gen_table_by_name(table_name) + gen_table_model = await GenTableCRUD(auth=self.auth).get_gen_table_by_name(table_name) # 检查表是否存在 if gen_table_model is None: raise CustomException(msg=f"业务表 {table_name} 不存在") gen_table = GenTableOutSchema.model_validate(gen_table_model) # 生成代码时按“上级目录”规则矫正最终包名(不落库,仅影响本次生成/预览/下载/写入) - gen_table.package_name = await cls._effective_package_name(auth, gen_table.parent_menu_id, gen_table.package_name) - await cls.set_pk_column(gen_table) - await cls.hydrate_sub_table(auth, gen_table) - cls._assert_master_sub_config_valid(gen_table) + gen_table.package_name = await self._effective_package_name(gen_table.parent_menu_id, gen_table.package_name) + await self.set_pk_column(gen_table) + await self.hydrate_sub_table(gen_table) + self._assert_master_sub_config_valid(gen_table) context = Jinja2TemplateUtil.prepare_context(gen_table) template_list = Jinja2TemplateUtil.get_template_list() output_files = [Jinja2TemplateUtil.get_file_name(template, gen_table) for template in template_list] @@ -1333,9 +1312,8 @@ class GenTableService: class GenTableColumnService: """代码生成业务表字段服务层""" - @classmethod @handle_service_exception - async def get_gen_table_column_list_by_table_id_service(cls, auth: AuthSchema, table_id: int) -> list[dict[str, Any]]: + async def get_gen_table_column_list_by_table_id(self, table_id: int) -> list[dict[str, Any]]: """获取业务表字段列表信息(输出模型)。 参数: @@ -1345,6 +1323,6 @@ class GenTableColumnService: 返回: - list[dict[str, Any]]: 业务表字段列表,每个元素为字段详细信息字典。 """ - gen_table_column_list_result = await GenTableColumnCRUD(auth).list_gen_table_column_crud({"table_id": table_id}) + gen_table_column_list_result = await GenTableColumnCRUD(auth=self.auth).list_gen_table_column_crud({"table_id": table_id}) result = [GenTableColumnOutSchema.model_validate(gen_table_column) for gen_table_column in gen_table_column_list_result] return result diff --git a/backend/app/plugin/module_task/cronjob/job/controller.py b/backend/app/plugin/module_task/cronjob/job/controller.py index b9f5e78a..11bea6c8 100644 --- a/backend/app/plugin/module_task/cronjob/job/controller.py +++ b/backend/app/plugin/module_task/cronjob/job/controller.py @@ -13,9 +13,7 @@ from app.core.router_class import OperationLogRoute from .schema import JobOutSchema, JobQueryParam from .service import JobService -JobRouter = APIRouter(route_class=OperationLogRoute, prefix="/cronjob/job", tags=["调度器监控"]) - -# ==================== 调度器状态和操作 ==================== +JobRouter = APIRouter(route_class=OperationLogRoute, prefix="/cronjob/job", tags=["任务调度", "调度器监控"]) @JobRouter.get( @@ -25,13 +23,7 @@ JobRouter = APIRouter(route_class=OperationLogRoute, prefix="/cronjob/job", tags dependencies=[Depends(AuthPermission(["module_task:cronjob:job:query"]))], ) async def get_scheduler_status_controller() -> JSONResponse: - """ - 获取调度器状态 - - 返回: - - JSONResponse: 调度器状态信息 - """ - data = JobService.get_scheduler_status_service() + data = JobService.get_scheduler_status() return SuccessResponse(data=data, msg="获取调度器状态成功") @@ -42,13 +34,7 @@ async def get_scheduler_status_controller() -> JSONResponse: dependencies=[Depends(AuthPermission(["module_task:cronjob:job:query"]))], ) async def get_scheduler_jobs_controller() -> JSONResponse: - """ - 获取调度器中的任务列表 - - 返回: - - JSONResponse: 调度器任务列表 - """ - data = JobService.get_scheduler_jobs_service() + data = JobService.get_scheduler_jobs() return SuccessResponse(data=data, msg="获取调度器任务列表成功") @@ -59,12 +45,6 @@ async def get_scheduler_jobs_controller() -> JSONResponse: dependencies=[Depends(AuthPermission(["module_task:cronjob:job:scheduler"]))], ) async def start_scheduler_controller() -> JSONResponse: - """ - 启动调度器。 - - 返回: - - JSONResponse: 成功提示响应。 - """ SchedulerUtil.start() return SuccessResponse(msg="调度器已启动") @@ -76,12 +56,6 @@ async def start_scheduler_controller() -> JSONResponse: dependencies=[Depends(AuthPermission(["module_task:cronjob:job:scheduler"]))], ) async def pause_scheduler_controller() -> JSONResponse: - """ - 暂停调度器。 - - 返回: - - JSONResponse: 成功提示响应。 - """ SchedulerUtil.pause() return SuccessResponse(msg="调度器已暂停") @@ -93,12 +67,6 @@ async def pause_scheduler_controller() -> JSONResponse: dependencies=[Depends(AuthPermission(["module_task:cronjob:job:scheduler"]))], ) async def resume_scheduler_controller() -> JSONResponse: - """ - 恢复调度器。 - - 返回: - - JSONResponse: 成功提示响应。 - """ SchedulerUtil.resume() return SuccessResponse(msg="调度器已恢复") @@ -110,12 +78,6 @@ async def resume_scheduler_controller() -> JSONResponse: dependencies=[Depends(AuthPermission(["module_task:cronjob:job:scheduler"]))], ) async def shutdown_scheduler_controller() -> JSONResponse: - """ - 关闭调度器。 - - 返回: - - JSONResponse: 成功提示响应。 - """ await SchedulerUtil.shutdown() return SuccessResponse(msg="调度器已关闭") @@ -127,12 +89,6 @@ async def shutdown_scheduler_controller() -> JSONResponse: dependencies=[Depends(AuthPermission(["module_task:cronjob:job:task"]))], ) async def clear_jobs_controller() -> JSONResponse: - """ - 清空调度器中的所有任务。 - - 返回: - - JSONResponse: 成功提示响应。 - """ SchedulerUtil.clear_jobs() return SuccessResponse(msg="已清空所有任务") @@ -144,12 +100,6 @@ async def clear_jobs_controller() -> JSONResponse: dependencies=[Depends(AuthPermission(["module_task:cronjob:job:query"]))], ) async def get_scheduler_console_controller() -> JSONResponse: - """ - 获取调度器控制台信息 - - 返回: - - JSONResponse: 调度器任务的控制台输出 - """ console_output = SchedulerUtil.print_jobs() return SuccessResponse(data=console_output, msg="获取控制台信息成功") @@ -161,19 +111,10 @@ async def get_scheduler_console_controller() -> JSONResponse: dependencies=[Depends(AuthPermission(["module_task:cronjob:job:update"]))], ) async def sync_jobs_controller() -> JSONResponse: - """ - 同步调度器任务到数据库 - - 返回: - - JSONResponse: 同步的任务数量 - """ sync_count = SchedulerUtil.sync_jobs_to_db() return SuccessResponse(data=sync_count, msg=f"同步完成,共同步 {sync_count} 个任务") -# ==================== 调度器任务操作 ==================== - - @JobRouter.post( "/task/pause/{job_id}", summary="暂停任务", @@ -183,15 +124,6 @@ async def sync_jobs_controller() -> JSONResponse: async def pause_job_controller( job_id: Annotated[str, Path(description="调度器任务ID")], ) -> JSONResponse: - """ - 暂停调度器中的任务 - - 参数: - - job_id (str): 调度器任务ID - - 返回: - - JSONResponse: 成功提示响应。 - """ SchedulerUtil.pause_job(job_id=job_id) return SuccessResponse(msg="暂停任务成功") @@ -205,15 +137,6 @@ async def pause_job_controller( async def resume_job_controller( job_id: Annotated[str, Path(description="调度器任务ID")], ) -> JSONResponse: - """ - 恢复调度器中的任务 - - 参数: - - job_id (str): 调度器任务ID - - 返回: - - JSONResponse: 成功提示响应。 - """ SchedulerUtil.resume_job(job_id=job_id) return SuccessResponse(msg="恢复任务成功") @@ -227,15 +150,6 @@ async def resume_job_controller( async def run_job_controller( job_id: Annotated[str, Path(description="调度器任务ID")], ) -> JSONResponse: - """ - 立即执行调度器中的任务 - - 参数: - - job_id (str): 调度器任务ID - - 返回: - - JSONResponse: 成功提示响应。 - """ SchedulerUtil.run_job_now(job_id=job_id) return SuccessResponse(msg="立即执行任务成功") @@ -249,22 +163,10 @@ async def run_job_controller( async def remove_job_controller( job_id: Annotated[str, Path(description="调度器任务ID")], ) -> JSONResponse: - """ - 从调度器中移除任务 - - 参数: - - job_id (str): 调度器任务ID - - 返回: - - JSONResponse: 成功提示响应。 - """ SchedulerUtil.remove_job(job_id=job_id) return SuccessResponse(msg="移除任务成功") -# ==================== 执行日志 ==================== - - @JobRouter.get( "/log/list", summary="查询执行日志列表", @@ -275,22 +177,10 @@ async def get_job_log_list_controller( search: Annotated[JobQueryParam, Depends()], auth: Annotated[AuthSchema, Depends(AuthPermission(["module_task:cronjob:job:query"]))], ) -> JSONResponse: - """ - 查询执行日志列表 - - 参数: - - page (PaginationQueryParam): 分页查询参数模型 - - search (JobQueryParam): 查询参数模型 - - auth (AuthSchema): 认证信息模型 - - 返回: - - JSONResponse: 包含分页后的执行日志列表 - """ order_by = [{"created_time": "desc"}] if page.order_by: order_by = page.order_by - result_dict = await JobService.get_job_log_page_service( - auth=auth, + result_dict = await JobService(auth).get_job_log_page( page_no=page.page_no, page_size=page.page_size, search=search, @@ -308,17 +198,7 @@ async def get_job_log_detail_controller( id: Annotated[int, Path(description="日志ID")], auth: Annotated[AuthSchema, Depends(AuthPermission(["module_task:cronjob:job:detail"]))], ) -> JSONResponse: - """ - 获取执行日志详情 - - 参数: - - id (int): 日志ID - - auth (AuthSchema): 认证信息模型 - - 返回: - - JSONResponse: 包含执行日志详情 - """ - result_dict = await JobService.get_job_log_detail_service(id=id, auth=auth) + result_dict = await JobService(auth).get_job_log_detail(id=id) return SuccessResponse(data=result_dict, msg="获取执行日志详情成功") @@ -331,15 +211,5 @@ async def delete_job_log_controller( ids: Annotated[list[int], Body(description="ID列表")], auth: Annotated[AuthSchema, Depends(AuthPermission(["module_task:cronjob:job:delete"]))], ) -> JSONResponse: - """ - 删除执行日志 - - 参数: - - ids (list[int]): ID列表 - - auth (AuthSchema): 认证信息模型 - - 返回: - - JSONResponse: 成功提示响应。 - """ - await JobService.delete_job_log_service(auth=auth, ids=ids) + await JobService(auth).delete_job_log(ids=ids) return SuccessResponse(msg="删除执行日志成功") diff --git a/backend/app/plugin/module_task/cronjob/job/service.py b/backend/app/plugin/module_task/cronjob/job/service.py index 028a1a17..38a3a96b 100644 --- a/backend/app/plugin/module_task/cronjob/job/service.py +++ b/backend/app/plugin/module_task/cronjob/job/service.py @@ -7,80 +7,37 @@ from .schema import JobCreateSchema, JobOutSchema, JobQueryParam, JobUpdateSchem class JobService: - """ - 调度器监控模块服务层 + """调度器监控模块服务层""" - 职责: - 1. 执行日志的 CRUD 操作 - 2. 调度器状态和任务列表的获取 - 3. 任务操作(暂停、恢复、执行、移除) - """ + def __init__(self, auth: AuthSchema) -> None: + self.auth = auth - @classmethod - async def get_job_log_detail_service(cls, auth: AuthSchema, id: int) -> JobOutSchema: - """ - 获取执行日志详情 - - 参数: - - auth (AuthSchema): 认证信息模型 - - id (int): 日志ID - - 返回: - - Dict: 执行日志详情字典 - """ - obj = await JobCRUD(auth).get_obj_by_id_crud(id=id) + async def get_job_log_detail(self, id: int) -> JobOutSchema: + obj = await JobCRUD(self.auth).get_obj_by_id_crud(id=id) if not obj: raise CustomException(msg="执行日志不存在") return JobOutSchema.model_validate(obj) - @classmethod - async def get_job_log_list_service( - cls, - auth: AuthSchema, + async def get_job_log_list( + self, search: JobQueryParam | None = None, order_by: list[dict[str, str]] | None = None, ) -> list[dict]: - """ - 获取执行日志列表 - - 参数: - - auth (AuthSchema): 认证信息模型 - - search (JobQueryParam | None): 查询参数模型 - - order_by (list[dict[str, str]] | None): 排序参数列表 - - 返回: - - List[Dict]: 执行日志详情字典列表 - """ if order_by is None: order_by = [{"created_time": "desc"}] - obj_list = await JobCRUD(auth).get_obj_list_crud(search=vars(search) if search else None, order_by=order_by) + obj_list = await JobCRUD(self.auth).get_obj_list_crud(search=vars(search) if search else None, order_by=order_by) return [JobOutSchema.model_validate(obj) for obj in obj_list] - @classmethod - async def get_job_log_page_service( - cls, - auth: AuthSchema, + async def get_job_log_page( + self, page_no: int, page_size: int, search: JobQueryParam | None = None, order_by: list[dict[str, str]] | None = None, ) -> dict: - """ - 分页查询执行日志(数据库 OFFSET/LIMIT)。 - - 参数: - - auth (AuthSchema): 认证信息。 - - page_no (int): 页码。 - - page_size (int): 每页条数。 - - search (JobQueryParam | None): 查询条件。 - - order_by (list[dict[str, str]] | None): 排序。 - - 返回: - - dict: 分页结果。 - """ offset = (page_no - 1) * page_size ob = order_by or [{"created_time": "desc"}] - return await JobCRUD(auth).page( + return await JobCRUD(self.auth).page( offset=offset, limit=page_size, order_by=ob, @@ -88,124 +45,61 @@ class JobService: out_schema=JobOutSchema, ) - @classmethod - async def create_job_log_service( - cls, - auth: AuthSchema, + async def create_job_log( + self, job_id: str, job_name: str | None = None, trigger_type: str | None = None, ) -> dict: - """ - 创建执行日志 - - 参数: - - auth (AuthSchema): 认证信息模型 - - job_id (str): 任务ID - - job_name (str | None): 任务名称 - - trigger_type (str | None): 触发方式 - - 返回: - - Dict: 执行日志详情字典 - """ data = JobCreateSchema( job_id=job_id, job_name=job_name, trigger_type=trigger_type, status="running", ) - obj = await JobCRUD(auth).create_obj_crud(data=data) + obj = await JobCRUD(self.auth).create_obj_crud(data=data) if not obj: raise CustomException(msg="创建执行日志失败") return JobOutSchema.model_validate(obj) - @classmethod - async def update_job_log_service( - cls, - auth: AuthSchema, + async def update_job_log( + self, id: int, status: str, result: str | None = None, error: str | None = None, ) -> dict: - """ - 更新执行日志 - - 参数: - - auth (AuthSchema): 认证信息模型 - - id (int): 日志ID - - status (str): 执行状态 - - result (str | None): 执行结果 - - error (str | None): 错误信息 - - 返回: - - Dict: 执行日志详情字典 - """ data = JobUpdateSchema( status=status, result=result, error=error, ) - obj = await JobCRUD(auth).update_obj_crud(id=id, data=data) + obj = await JobCRUD(self.auth).update_obj_crud(id=id, data=data) if not obj: raise CustomException(msg="更新执行日志失败") return JobOutSchema.model_validate(obj) - @classmethod - async def delete_job_log_service(cls, auth: AuthSchema, ids: list[int]) -> None: - """ - 删除执行日志 - - 参数: - - auth (AuthSchema): 认证信息模型 - - ids (list[int]): 日志ID列表 - - 返回: - - None - """ + async def delete_job_log(self, ids: list[int]) -> None: if len(ids) < 1: raise CustomException(msg="删除失败,删除对象不能为空") - await JobCRUD(auth).delete_obj_crud(ids=ids) + await JobCRUD(self.auth).delete_obj_crud(ids=ids) - @classmethod - async def clear_job_log_service(cls, auth: AuthSchema) -> None: - """ - 清空所有执行日志 + async def clear_job_log(self) -> None: + await JobCRUD(self.auth).clear_obj_crud() - 参数: - - auth (AuthSchema): 认证信息模型 - - 返回: - - None - """ - await JobCRUD(auth).clear_obj_crud() - - @classmethod - def get_scheduler_status_service(cls) -> dict: - """ - 获取调度器状态 - - 返回: - - Dict: 调度器状态信息 - """ + @staticmethod + def get_scheduler_status() -> dict: status = SchedulerUtil.get_scheduler_state() is_running = SchedulerUtil.is_running() jobs = SchedulerUtil.get_all_jobs() - return { "status": status, "is_running": is_running, "job_count": len(jobs), } - @classmethod - def get_scheduler_jobs_service(cls) -> list[dict]: - """ - 获取调度器中的任务列表 - - 返回: - - List[Dict]: 任务列表 - """ + @staticmethod + def get_scheduler_jobs() -> list[dict]: jobs = SchedulerUtil.get_all_jobs() return [ { diff --git a/backend/app/plugin/module_task/cronjob/node/controller.py b/backend/app/plugin/module_task/cronjob/node/controller.py index bd4c7904..76f538ac 100644 --- a/backend/app/plugin/module_task/cronjob/node/controller.py +++ b/backend/app/plugin/module_task/cronjob/node/controller.py @@ -18,7 +18,7 @@ from .schema import ( ) from .service import NodeService -NodeRouter = APIRouter(route_class=OperationLogRoute, prefix="/cronjob/node", tags=["节点管理"]) +NodeRouter = APIRouter(route_class=OperationLogRoute, prefix="/cronjob/node", tags=["任务调度", "节点管理"]) @NodeRouter.get( @@ -29,16 +29,8 @@ NodeRouter = APIRouter(route_class=OperationLogRoute, prefix="/cronjob/node", ta async def get_node_options_controller( auth: Annotated[AuthSchema, Depends(AuthPermission(["module_task:cronjob:node:query"]))], ) -> JSONResponse: - """ - 获取数据库中的定时任务节点定义(task_node),与编排节点类型无关。 - - 参数: - - auth (AuthSchema): 认证信息。 - - 返回: - - JSONResponse: 成功响应,data 为选项列表。 - """ - result = await NodeService.get_node_options_service(auth=auth) + service = NodeService(auth) + result = await service.options() return SuccessResponse(data=result, msg="获取定时任务节点选项成功") @@ -51,17 +43,8 @@ async def get_obj_detail_controller( id: Annotated[int, Path(description="节点ID")], auth: Annotated[AuthSchema, Depends(AuthPermission(["module_task:cronjob:node:detail"]))], ) -> JSONResponse: - """ - 获取节点详情 - - 参数: - - id (int): 节点ID - - auth (AuthSchema): 认证信息模型 - - 返回: - - JSONResponse: 包含节点详情的JSON响应 - """ - result_dict = await NodeService.get_node_detail_service(id=id, auth=auth) + service = NodeService(auth) + result_dict = await service.detail(id=id) return SuccessResponse(data=result_dict, msg="获取节点详情成功") @@ -75,19 +58,8 @@ async def get_obj_list_controller( search: Annotated[NodeQueryParam, Depends()], auth: Annotated[AuthSchema, Depends(AuthPermission(["module_task:cronjob:node:query"]))], ) -> JSONResponse: - """ - 查询节点 - - 参数: - - page (PaginationQueryParam): 分页查询参数模型 - - search (NodeQueryParam): 查询参数模型 - - auth (AuthSchema): 认证信息模型 - - 返回: - - JSONResponse: 包含分页后的节点列表的JSON响应 - """ - result_dict = await NodeService.get_node_page_service( - auth=auth, + service = NodeService(auth) + result_dict = await service.page( page_no=page.page_no, page_size=page.page_size, search=search, @@ -105,17 +77,8 @@ async def create_obj_controller( data: NodeCreateSchema, auth: Annotated[AuthSchema, Depends(AuthPermission(["module_task:cronjob:node:create"]))], ) -> JSONResponse: - """ - 创建节点 - - 参数: - - data (NodeCreateSchema): 创建参数模型 - - auth (AuthSchema): 认证信息模型 - - 返回: - - JSONResponse: 包含创建节点结果的JSON响应 - """ - result_dict = await NodeService.create_node_service(auth=auth, data=data) + service = NodeService(auth) + result_dict = await service.create(data=data) return SuccessResponse(data=result_dict, msg="创建节点成功") @@ -129,18 +92,8 @@ async def update_obj_controller( id: Annotated[int, Path(description="节点ID")], auth: Annotated[AuthSchema, Depends(AuthPermission(["module_task:cronjob:node:update"]))], ) -> JSONResponse: - """ - 修改节点 - - 参数: - - data (NodeUpdateSchema): 更新参数模型 - - id (int): 节点ID - - auth (AuthSchema): 认证信息模型 - - 返回: - - JSONResponse: 包含修改节点结果的JSON响应 - """ - result_dict = await NodeService.update_node_service(auth=auth, id=id, data=data) + service = NodeService(auth) + result_dict = await service.update(id=id, data=data) return SuccessResponse(data=result_dict, msg="修改节点成功") @@ -153,17 +106,8 @@ async def delete_obj_controller( ids: Annotated[list[int], Body(description="ID列表")], auth: Annotated[AuthSchema, Depends(AuthPermission(["module_task:cronjob:node:delete"]))], ) -> JSONResponse: - """ - 删除节点 - - 参数: - - ids (list[int]): ID列表 - - auth (AuthSchema): 认证信息模型 - - 返回: - - JSONResponse: 包含删除节点结果的JSON响应 - """ - await NodeService.delete_node_service(auth=auth, ids=ids) + service = NodeService(auth) + await service.delete(ids=ids) return SuccessResponse(msg="删除节点成功") @@ -175,16 +119,8 @@ async def delete_obj_controller( async def clear_obj_controller( auth: Annotated[AuthSchema, Depends(AuthPermission(["module_task:cronjob:node:delete"]))], ) -> JSONResponse: - """ - 清空所有节点 - - 参数: - - auth (AuthSchema): 认证信息模型 - - 返回: - - JSONResponse: 包含清空节点结果的JSON响应 - """ - await NodeService.clear_node_service(auth=auth) + service = NodeService(auth) + await service.clear() return SuccessResponse(msg="清空节点成功") @@ -198,18 +134,8 @@ async def execute_job_controller( data: NodeExecuteSchema, auth: Annotated[AuthSchema, Depends(AuthPermission(["module_task:cronjob:node:execute"]))], ) -> JSONResponse: - """ - 调试节点 - - 参数: - - id (int): 节点ID - - data (NodeExecuteSchema): 执行参数模型 - - auth (AuthSchema): 认证信息模型 - - 返回: - - JSONResponse: 包含调试结果的JSON响应 - """ - result = await NodeService.execute_node_service(auth=auth, id=id, execute_data=data) + service = NodeService(auth) + result = await service.execute(id=id, execute_data=data) return SuccessResponse(data=result, msg="调试节点成功") @@ -223,16 +149,6 @@ async def batch_set_status_controller( status: Annotated[str, Body(description="状态值")], auth: Annotated[AuthSchema, Depends(AuthPermission(["module_task:cronjob:node:update"]))], ) -> JSONResponse: - """ - 批量设置节点状态 - - 参数: - - ids (list[int]): 节点ID列表 - - status (str): 状态值 - - auth (AuthSchema): 认证信息模型 - - 返回: - - JSONResponse: 成功响应 - """ - await NodeService.batch_set_status_service(auth=auth, ids=ids, status=status) + service = NodeService(auth) + await service.batch_set_status(ids=ids, status=status) return SuccessResponse(msg="批量设置节点状态成功") diff --git a/backend/app/plugin/module_task/cronjob/node/service.py b/backend/app/plugin/module_task/cronjob/node/service.py index 9078165a..f4574830 100644 --- a/backend/app/plugin/module_task/cronjob/node/service.py +++ b/backend/app/plugin/module_task/cronjob/node/service.py @@ -16,29 +16,13 @@ from .schema import ( class NodeService: - """ - 节点管理模块服务层 + """节点管理模块服务层""" - 设计原则: - 1. 节点CRUD只操作数据库,不直接操作调度器 - 2. 调度器节点通过"执行"操作来创建和管理 - 3. 支持预设函数和自定义代码块两种执行方式 - """ + def __init__(self, auth: AuthSchema) -> None: + self.auth = auth - @classmethod - async def get_node_options_service(cls, auth: AuthSchema) -> list[dict]: - """ - 获取定时任务节点(task_node)列表,供调度/调试使用。 - - 工作流画布请使用 ``app.plugin.module_task.workflow.node_type`` 模块下接口(/task/workflow/node-type/options)。 - - 参数: - - auth (AuthSchema): 认证信息模型 - - 返回: - - list[dict]: 节点类型选项列表 - """ - obj_list = await NodeCRUD(auth).get_obj_list_crud() + async def options(self) -> list[dict]: + obj_list = await NodeCRUD(self.auth).get_obj_list_crud() return [ { "id": obj.id, @@ -51,66 +35,27 @@ class NodeService: for obj in obj_list ] - @classmethod - async def get_node_detail_service(cls, auth: AuthSchema, id: int) -> NodeOutSchema: - """ - 获取节点详情 - - 参数: - - auth (AuthSchema): 认证信息模型 - - id (int): 节点ID - - 返回: - - Dict: 节点详情字典 - """ - obj = await NodeCRUD(auth).get_obj_by_id_crud(id=id) + async def detail(self, id: int) -> NodeOutSchema: + obj = await NodeCRUD(self.auth).get_obj_by_id_crud(id=id) return NodeOutSchema.model_validate(obj) - @classmethod - async def get_node_list_service( - cls, - auth: AuthSchema, + async def list( + self, search: NodeQueryParam | None = None, order_by: list[dict[str, str]] | None = None, ) -> list[dict]: - """ - 获取节点列表 - - 参数: - - auth (AuthSchema): 认证信息模型 - - search (NodeQueryParam | None): 查询参数模型 - - order_by (list[dict[str, str]] | None): 排序参数列表 - - 返回: - - List[Dict]: 节点详情字典列表 - """ - obj_list = await NodeCRUD(auth).get_obj_list_crud(search=vars(search) if search else None, order_by=order_by) + obj_list = await NodeCRUD(self.auth).get_obj_list_crud(search=vars(search) if search else None, order_by=order_by) return [NodeOutSchema.model_validate(obj) for obj in obj_list] - @classmethod - async def get_node_page_service( - cls, - auth: AuthSchema, + async def page( + self, page_no: int, page_size: int, search: NodeQueryParam | None = None, order_by: list[dict[str, str]] | None = None, ) -> dict: - """ - 分页查询定时任务节点(数据库 OFFSET/LIMIT)。 - - 参数: - - auth (AuthSchema): 认证信息。 - - page_no (int): 页码。 - - page_size (int): 每页条数。 - - search (NodeQueryParam | None): 查询条件。 - - order_by (list[dict[str, str]] | None): 排序。 - - 返回: - - dict: 分页结果。 - """ offset = (page_no - 1) * page_size - return await NodeCRUD(auth).page( + return await NodeCRUD(self.auth).page( offset=offset, limit=page_size, order_by=order_by or [{"id": "asc"}], @@ -118,102 +63,45 @@ class NodeService: out_schema=NodeOutSchema, ) - @classmethod - async def create_node_service(cls, auth: AuthSchema, data: NodeCreateSchema) -> NodeOutSchema: - """ - 创建节点 - 只保存到数据库,不创建调度器任务 - - 参数: - - auth (AuthSchema): 认证信息模型 - - data (NodeCreateSchema): 节点创建模型 - - 返回: - - Dict: 节点详情字典 - """ - exist_obj = await NodeCRUD(auth).get(name=data.name) + async def create(self, data: NodeCreateSchema) -> NodeOutSchema: + exist_obj = await NodeCRUD(self.auth).get(name=data.name) if exist_obj: raise CustomException(msg="创建失败,该节点已存在") - obj = await NodeCRUD(auth).create_obj_crud(data=data) + obj = await NodeCRUD(self.auth).create_obj_crud(data=data) if not obj: raise CustomException(msg="创建失败") return NodeOutSchema.model_validate(obj) - @classmethod - async def update_node_service(cls, auth: AuthSchema, id: int, data: NodeUpdateSchema) -> NodeOutSchema: - """ - 更新节点 - 只更新数据库,不修改调度器任务 - - 参数: - - auth (AuthSchema): 认证信息模型 - - id (int): 节点ID - - data (NodeUpdateSchema): 节点更新模型 - - 返回: - - dict: 节点详情字典 - """ - exist_obj = await NodeCRUD(auth).get_obj_by_id_crud(id=id) + async def update(self, id: int, data: NodeUpdateSchema) -> NodeOutSchema: + exist_obj = await NodeCRUD(self.auth).get_obj_by_id_crud(id=id) if not exist_obj: raise CustomException(msg="更新失败,该节点不存在") - obj = await NodeCRUD(auth).update_obj_crud(id=id, data=data) + obj = await NodeCRUD(self.auth).update_obj_crud(id=id, data=data) if not obj: raise CustomException(msg="更新失败") return NodeOutSchema.model_validate(obj) - @classmethod - async def delete_node_service(cls, auth: AuthSchema, ids: list[int]) -> None: - """ - 删除节点 - 只删除数据库记录,同时移除调度器中的任务 - - 参数: - - auth (AuthSchema): 认证信息模型 - - ids (list[int]): 节点ID列表 - - 返回: - - None - """ + async def delete(self, ids: list[int]) -> None: if len(ids) < 1: raise CustomException(msg="删除失败,删除对象不能为空") - for id in ids: - exist_obj = await NodeCRUD(auth).get_obj_by_id_crud(id=id) + for mid in ids: + exist_obj = await NodeCRUD(self.auth).get_obj_by_id_crud(id=mid) if not exist_obj: raise CustomException(msg="删除失败,该节点不存在") try: - SchedulerUtil.remove_job(job_id=id) + SchedulerUtil.remove_job(job_id=mid) except JobLookupError: - # 作业不存在,忽略异常,继续删除数据库记录 pass - await NodeCRUD(auth).delete_obj_crud(ids=ids) + await NodeCRUD(self.auth).delete_obj_crud(ids=ids) - @classmethod - async def clear_node_service(cls, auth: AuthSchema) -> None: - """ - 清空所有节点 - - 参数: - - auth (AuthSchema): 认证信息模型 - - 返回: - - None - """ + async def clear(self) -> None: SchedulerUtil.clear_jobs() - await NodeCRUD(auth).clear_obj_crud() + await NodeCRUD(self.auth).clear_obj_crud() - @classmethod - async def execute_node_service(cls, auth: AuthSchema, id: int, execute_data: NodeExecuteSchema) -> dict: - """ - 调试节点 - 根据任务配置创建调度器任务并执行 - - 参数: - - auth (AuthSchema): 认证信息模型 - - id (int): 节点ID - - execute_data (NodeExecuteSchema): 执行参数模型 - - 返回: - - dict: 调试结果 - """ - obj = await NodeCRUD(auth).get_obj_by_id_crud(id=id) + async def execute(self, id: int, execute_data: NodeExecuteSchema) -> dict: + obj = await NodeCRUD(self.auth).get_obj_by_id_crud(id=id) if not obj: raise CustomException(msg="调试失败,该节点不存在") @@ -253,23 +141,11 @@ class NodeService: return {"job_id": id, "status": "executed", "trigger": trigger} - @classmethod - async def batch_set_status_service(cls, auth: AuthSchema, ids: list[int], status: str) -> None: - """ - 批量设置节点状态 - - 参数: - - auth (AuthSchema): 认证信息模型 - - ids (list[int]): 节点ID列表 - - status (str): 状态值 - - 返回: - - None - """ + async def batch_set_status(self, ids: list[int], status: str) -> None: if not ids: raise CustomException(msg="请选择要操作的数据") - await NodeCRUD(auth).update_obj_crud( + await NodeCRUD(self.auth).update_obj_crud( ids=ids, data={"status": status}, ) diff --git a/backend/app/plugin/module_task/plugin.toml b/backend/app/plugin/module_task/plugin.toml index 3c96f7bd..f18d46d7 100644 --- a/backend/app/plugin/module_task/plugin.toml +++ b/backend/app/plugin/module_task/plugin.toml @@ -3,6 +3,6 @@ name = "task" title = "任务与工作流" version = "1.0.0" -description = "定时任务、Prefect 工作流等子模块。" +description = "定时任务、DAG 工作流等子模块。" optional = true tags = ["task", "cron", "workflow"] diff --git a/backend/app/plugin/module_task/workflow/__init__.py b/backend/app/plugin/module_task/workflow/__init__.py index 3125c553..a3380862 100644 --- a/backend/app/plugin/module_task/workflow/__init__.py +++ b/backend/app/plugin/module_task/workflow/__init__.py @@ -3,7 +3,7 @@ - ``definition``: 工作流定义(画布 CRUD、发布、执行 API) - ``node_type``: 编排节点类型(palette / 与 task_node 分离) -- ``engine``: Prefect DAG 执行引擎 +- ``engine``: 拓扑分层并行执行引擎 动态路由仍统一挂在 ``/task`` 下(见各子包 ``controller.py`` 的 ``prefix``)。 """ diff --git a/backend/app/plugin/module_task/workflow/flows/controller.py b/backend/app/plugin/module_task/workflow/flows/controller.py index b42c086f..22a558fd 100644 --- a/backend/app/plugin/module_task/workflow/flows/controller.py +++ b/backend/app/plugin/module_task/workflow/flows/controller.py @@ -19,7 +19,7 @@ from .schema import ( ) from .service import WorkflowService -WorkflowRouter = APIRouter(route_class=OperationLogRoute, prefix="/workflow/definition", tags=["工作流"]) +WorkflowRouter = APIRouter(route_class=OperationLogRoute, prefix="/workflow/definition", tags=["任务调度", "工作流"]) @WorkflowRouter.get( @@ -31,17 +31,7 @@ async def get_workflow_detail_controller( id: Annotated[int, Path(description="工作流ID")], auth: Annotated[AuthSchema, Depends(AuthPermission(["module_task:workflow:definition:detail"]))], ) -> JSONResponse: - """ - 根据 ID 获取工作流详情(含画布 nodes/edges)。 - - 参数: - - id (int): 工作流 ID。 - - auth (AuthSchema): 认证信息。 - - 返回: - - JSONResponse: 成功响应,data 为详情字典。 - """ - result_dict = await WorkflowService.get_workflow_detail_service(auth=auth, id=id) + result_dict = await WorkflowService(auth).get_workflow_detail(id=id) return SuccessResponse(data=result_dict, msg="获取工作流详情成功") @@ -55,19 +45,7 @@ async def get_workflow_list_controller( search: Annotated[WorkflowQueryParam, Depends()], auth: Annotated[AuthSchema, Depends(AuthPermission(["module_task:workflow:definition:query"]))], ) -> JSONResponse: - """ - 分页查询工作流列表。 - - 参数: - - page (PaginationQueryParam): 分页与排序参数。 - - search (WorkflowQueryParam): 查询条件。 - - auth (AuthSchema): 认证信息。 - - 返回: - - JSONResponse: 成功响应,data 为分页结果。 - """ - result_dict = await WorkflowService.get_workflow_page_service( - auth=auth, + result_dict = await WorkflowService(auth).get_workflow_page( page_no=page.page_no, page_size=page.page_size, search=search, @@ -85,17 +63,7 @@ async def create_workflow_controller( data: WorkflowCreateSchema, auth: Annotated[AuthSchema, Depends(AuthPermission(["module_task:workflow:definition:create"]))], ) -> JSONResponse: - """ - 创建草稿工作流。 - - 参数: - - data (WorkflowCreateSchema): 创建体。 - - auth (AuthSchema): 认证信息。 - - 返回: - - JSONResponse: 成功响应,data 为新建工作流。 - """ - result_dict = await WorkflowService.create_workflow_service(auth=auth, data=data) + result_dict = await WorkflowService(auth).create_workflow(data=data) return SuccessResponse(data=result_dict, msg="创建工作流成功") @@ -109,18 +77,7 @@ async def update_workflow_controller( data: WorkflowUpdateSchema, auth: Annotated[AuthSchema, Depends(AuthPermission(["module_task:workflow:definition:update"]))], ) -> JSONResponse: - """ - 更新工作流及画布。 - - 参数: - - id (int): 工作流 ID。 - - data (WorkflowUpdateSchema): 更新体。 - - auth (AuthSchema): 认证信息。 - - 返回: - - JSONResponse: 成功响应,data 为更新后的工作流。 - """ - result_dict = await WorkflowService.update_workflow_service(auth=auth, id=id, data=data) + result_dict = await WorkflowService(auth).update_workflow(id=id, data=data) return SuccessResponse(data=result_dict, msg="更新工作流成功") @@ -133,17 +90,7 @@ async def delete_workflow_controller( ids: Annotated[list[int], Body(description="ID列表")], auth: Annotated[AuthSchema, Depends(AuthPermission(["module_task:workflow:definition:delete"]))], ) -> JSONResponse: - """ - 批量删除工作流。 - - 参数: - - ids (list[int]): 工作流 ID 列表。 - - auth (AuthSchema): 认证信息。 - - 返回: - - JSONResponse: 成功提示响应。 - """ - await WorkflowService.delete_workflow_service(auth=auth, ids=ids) + await WorkflowService(auth).delete_workflow(ids=ids) return SuccessResponse(msg="删除工作流成功") @@ -156,17 +103,7 @@ async def publish_workflow_controller( id: Annotated[int, Path(description="工作流ID")], auth: Annotated[AuthSchema, Depends(AuthPermission(["module_task:workflow:definition:update"]))], ) -> JSONResponse: - """ - 校验 DAG 无环后发布工作流。 - - 参数: - - id (int): 工作流 ID。 - - auth (AuthSchema): 认证信息。 - - 返回: - - JSONResponse: 成功响应,data 为发布后工作流。 - """ - result_dict = await WorkflowService.publish_workflow_service(auth=auth, id=id) + result_dict = await WorkflowService(auth).publish_workflow(id=id) return SuccessResponse(data=result_dict, msg="发布工作流成功") @@ -179,15 +116,5 @@ async def execute_workflow_controller( body: WorkflowExecuteSchema, auth: Annotated[AuthSchema, Depends(AuthPermission(["module_task:workflow:definition:execute"]))], ) -> JSONResponse: - """ - 使用 Prefect 按拓扑顺序执行已发布工作流。 - - 参数: - - body (WorkflowExecuteSchema): 工作流 ID 与变量等。 - - auth (AuthSchema): 认证信息。 - - 返回: - - JSONResponse: 成功响应,data 为执行结果摘要。 - """ - result_dict = await WorkflowService.execute_workflow_service(auth=auth, body=body) + result_dict = await WorkflowService(auth).execute_workflow(body=body) return SuccessResponse(data=result_dict, msg="执行工作流完成") diff --git a/backend/app/plugin/module_task/workflow/flows/model.py b/backend/app/plugin/module_task/workflow/flows/model.py index 8cf669f4..757e68da 100644 --- a/backend/app/plugin/module_task/workflow/flows/model.py +++ b/backend/app/plugin/module_task/workflow/flows/model.py @@ -6,7 +6,7 @@ from app.core.base_model import ModelMixin, TenantMixin, UserMixin class WorkflowModel(ModelMixin, TenantMixin, UserMixin): """ - 工作流定义:Vue Flow 画布序列化 + Prefect 运行时执行 + 工作流定义:Vue Flow 画布序列化 + 拓扑分层并行执行 """ __tablename__: str = "task_workflow" diff --git a/backend/app/plugin/module_task/workflow/flows/service.py b/backend/app/plugin/module_task/workflow/flows/service.py index a8d6a96f..fadd6e22 100644 --- a/backend/app/plugin/module_task/workflow/flows/service.py +++ b/backend/app/plugin/module_task/workflow/flows/service.py @@ -4,8 +4,8 @@ from typing import Any from app.core.base_schema import AuthSchema from app.core.exceptions import CustomException -from ..engine.prefect_engine import run_prefect_workflow_sync, utc_now_iso, validate_workflow_graph -from ..node_type.crud import WorkflowNodeTypeCRUD +from ..handlers.workflow_engine import run_workflow_sync, utc_now_iso, validate_workflow_graph +from ..nodes.crud import WorkflowNodeTypeCRUD from .crud import WorkflowCRUD from .schema import ( WorkflowCreateSchema, @@ -18,83 +18,43 @@ from .schema import ( class WorkflowService: - """工作流:画布存储 + 发布校验 + Prefect 执行""" + """工作流:画布存储 + 发布校验 + 分层并行执行""" - @classmethod - def _out(cls, obj: Any) -> WorkflowOutSchema: + def __init__(self, auth: AuthSchema) -> None: + self.auth = auth + + def _out(self, obj: Any) -> WorkflowOutSchema: return WorkflowOutSchema.model_validate(obj) - @classmethod - async def get_workflow_detail_service(cls, auth: AuthSchema, id: int) -> WorkflowOutSchema: - """ - 获取工作流详情。 - - 参数: - - auth (AuthSchema): 认证信息。 - - id (int): 工作流 ID。 - - 返回: - - dict: 序列化后的工作流详情。 - - 异常: - - CustomException: 不存在时抛出。 - """ - obj = await WorkflowCRUD(auth).get_obj_by_id_crud(id=id) + async def get_workflow_detail(self, id: int) -> WorkflowOutSchema: + obj = await WorkflowCRUD(self.auth).get_obj_by_id_crud(id=id) if not obj: raise CustomException(msg="工作流不存在") - return cls._out(obj) + return self._out(obj) - @classmethod - async def get_workflow_list_service( - cls, - auth: AuthSchema, + async def get_workflow_list( + self, search: WorkflowQueryParam | None = None, order_by: list[dict[str, str]] | None = None, ) -> list[WorkflowOutSchema]: - """ - 获取工作流列表(非分页)。 - - 参数: - - auth (AuthSchema): 认证信息。 - - search (WorkflowQueryParam | None): 查询条件。 - - order_by (list[dict[str, str]] | None): 排序。 - - 返回: - - list[dict]: 工作流字典列表。 - """ if order_by is None: order_by = [{"updated_time": "desc"}] - obj_list = await WorkflowCRUD(auth).get_obj_list_crud( + obj_list = await WorkflowCRUD(self.auth).get_obj_list_crud( search=vars(search) if search else None, order_by=order_by, ) - return [cls._out(o) for o in obj_list] + return [self._out(o) for o in obj_list] - @classmethod - async def get_workflow_page_service( - cls, - auth: AuthSchema, + async def get_workflow_page( + self, page_no: int, page_size: int, search: WorkflowQueryParam | None = None, order_by: list[dict[str, str]] | None = None, ) -> dict: - """ - 分页查询工作流(数据库 OFFSET/LIMIT)。 - - 参数: - - auth (AuthSchema): 认证信息。 - - page_no (int): 页码。 - - page_size (int): 每页条数。 - - search (WorkflowQueryParam | None): 查询条件。 - - order_by (list[dict[str, str]] | None): 排序。 - - 返回: - - dict: 分页结果(items 已 JSON 友好序列化)。 - """ offset = (page_no - 1) * page_size order = order_by or [{"updated_time": "desc"}] - result = await WorkflowCRUD(auth).page( + result = await WorkflowCRUD(self.auth).page( offset=offset, limit=page_size, order_by=order, @@ -104,92 +64,35 @@ class WorkflowService: result.items = [WorkflowOutSchema.model_validate(item).model_dump(mode="json") for item in result.items] return result - @classmethod - async def create_workflow_service(cls, auth: AuthSchema, data: WorkflowCreateSchema) -> WorkflowOutSchema: - """ - 创建工作流草稿。 - - 参数: - - auth (AuthSchema): 认证信息。 - - data (WorkflowCreateSchema): 创建体。 - - 返回: - - dict: 新建工作流字典。 - - 异常: - - CustomException: 编码重复或创建失败。 - """ - exist = await WorkflowCRUD(auth).get(code=data.code) + async def create_workflow(self, data: WorkflowCreateSchema) -> WorkflowOutSchema: + exist = await WorkflowCRUD(self.auth).get(code=data.code) if exist: raise CustomException(msg="流程编码已存在") - obj = await WorkflowCRUD(auth).create_obj_crud(data=data) + obj = await WorkflowCRUD(self.auth).create_obj_crud(data=data) if not obj: raise CustomException(msg="创建工作流失败") - return cls._out(obj) + return self._out(obj) - @classmethod - async def update_workflow_service(cls, auth: AuthSchema, id: int, data: WorkflowUpdateSchema) -> WorkflowOutSchema: - """ - 更新工作流。 - - 参数: - - auth (AuthSchema): 认证信息。 - - id (int): 工作流 ID。 - - data (WorkflowUpdateSchema): 更新体。 - - 返回: - - dict: 更新后工作流字典。 - - 异常: - - CustomException: 不存在、编码冲突或更新失败。 - """ - exist = await WorkflowCRUD(auth).get_obj_by_id_crud(id=id) + async def update_workflow(self, id: int, data: WorkflowUpdateSchema) -> WorkflowOutSchema: + exist = await WorkflowCRUD(self.auth).get_obj_by_id_crud(id=id) if not exist: raise CustomException(msg="工作流不存在") if exist.code != data.code: - other = await WorkflowCRUD(auth).get(code=data.code) + other = await WorkflowCRUD(self.auth).get(code=data.code) if other: raise CustomException(msg="流程编码已存在") - obj = await WorkflowCRUD(auth).update_obj_crud(id=id, data=data) + obj = await WorkflowCRUD(self.auth).update_obj_crud(id=id, data=data) if not obj: raise CustomException(msg="更新工作流失败") - return cls._out(obj) + return self._out(obj) - @classmethod - async def delete_workflow_service(cls, auth: AuthSchema, ids: list[int]) -> None: - """ - 批量删除工作流。 - - 参数: - - auth (AuthSchema): 认证信息。 - - ids (list[int]): ID 列表。 - - 返回: - - None - - 异常: - - CustomException: ID 为空时抛出。 - """ + async def delete_workflow(self, ids: list[int]) -> None: if not ids: raise CustomException(msg="删除ID不能为空") - await WorkflowCRUD(auth).delete_obj_crud(ids=ids) + await WorkflowCRUD(self.auth).delete_obj_crud(ids=ids) - @classmethod - async def publish_workflow_service(cls, auth: AuthSchema, id: int) -> WorkflowOutSchema: - """ - 校验 DAG 后发布工作流。 - - 参数: - - auth (AuthSchema): 认证信息。 - - id (int): 工作流 ID。 - - 返回: - - dict: 发布后工作流字典。 - - 异常: - - CustomException: 不存在、图无效或发布失败。 - """ - obj = await WorkflowCRUD(auth).get_obj_by_id_crud(id=id) + async def publish_workflow(self, id: int) -> WorkflowOutSchema: + obj = await WorkflowCRUD(self.auth).get_obj_by_id_crud(id=id) if not obj: raise CustomException(msg="工作流不存在") nodes = obj.nodes or [] @@ -208,27 +111,13 @@ class WorkflowService: edges=obj.edges, workflow_status="published", ) - updated = await WorkflowCRUD(auth).update_obj_crud(id=id, data=data) + updated = await WorkflowCRUD(self.auth).update_obj_crud(id=id, data=data) if not updated: raise CustomException(msg="发布失败") - return cls._out(updated) + return self._out(updated) - @classmethod - async def execute_workflow_service(cls, auth: AuthSchema, body: WorkflowExecuteSchema) -> WorkflowExecuteResultSchema: - """ - 执行已发布工作流(Prefect 同步入口在线程池中运行)。 - - 参数: - - auth (AuthSchema): 认证信息。 - - body (WorkflowExecuteSchema): 工作流 ID 与变量。 - - 返回: - - dict: 执行结果摘要(成功或失败结构)。 - - 异常: - - CustomException: 未发布、缺节点、节点类型未注册等。 - """ - obj = await WorkflowCRUD(auth).get_obj_by_id_crud(id=body.workflow_id) + async def execute_workflow(self, body: WorkflowExecuteSchema) -> WorkflowExecuteResultSchema: + obj = await WorkflowCRUD(self.auth).get_obj_by_id_crud(id=body.workflow_id) if not obj: raise CustomException(msg="工作流不存在") if obj.workflow_status != "published": @@ -242,7 +131,7 @@ class WorkflowService: codes_set = {n.get("type") for n in nodes if n.get("type")} code_list = list(codes_set) templates: dict[str, dict[str, Any]] = {} - type_objs = await WorkflowNodeTypeCRUD(auth).get_obj_list_crud(search={"code": ("in", code_list)}) + type_objs = await WorkflowNodeTypeCRUD(self.auth).get_obj_list_crud(search={"code": ("in", code_list)}) type_map = {t.code: t for t in type_objs} for code in codes_set: node_type = type_map.get(code) @@ -260,7 +149,7 @@ class WorkflowService: start = utc_now_iso() try: raw = await asyncio.to_thread( - run_prefect_workflow_sync, + run_workflow_sync, nodes, edges, templates, diff --git a/backend/app/plugin/module_task/workflow/handlers/__init__.py b/backend/app/plugin/module_task/workflow/handlers/__init__.py index 5554d270..f5b34b70 100644 --- a/backend/app/plugin/module_task/workflow/handlers/__init__.py +++ b/backend/app/plugin/module_task/workflow/handlers/__init__.py @@ -1,9 +1,9 @@ -"""Prefect 编排执行(DAG 校验、拓扑排序、Flow/Task)。""" +"""工作流执行引擎(DAG 校验、拓扑排序、分层并行执行)。""" -from .prefect_engine import run_prefect_workflow_sync, utc_now_iso, validate_workflow_graph +from .workflow_engine import run_workflow_sync, utc_now_iso, validate_workflow_graph __all__ = [ - "run_prefect_workflow_sync", + "run_workflow_sync", "utc_now_iso", "validate_workflow_graph", ] diff --git a/backend/app/plugin/module_task/workflow/handlers/prefect_engine.py b/backend/app/plugin/module_task/workflow/handlers/prefect_engine.py deleted file mode 100644 index 954fd474..00000000 --- a/backend/app/plugin/module_task/workflow/handlers/prefect_engine.py +++ /dev/null @@ -1,232 +0,0 @@ -""" -将 Vue Flow 画布(nodes/edges)转为 DAG,按拓扑顺序用 Prefect 编排执行。 - -画布节点 `type` 对应表 task_workflow_node_type.code(与定时任务 task_node 无关), -执行时加载该类型的 `func` 代码块,经 SchedulerUtil._task_wrapper 运行。 -""" - -from __future__ import annotations - -import json -from collections import defaultdict, deque -from datetime import datetime, timezone -from typing import Any - -from prefect import flow, task - -from app.core.ap_scheduler import SchedulerUtil -from app.core.logger import logger - - -def _parse_args(args_str: str | None) -> list[Any]: - if not args_str or not str(args_str).strip(): - return [] - return [a.strip() for a in str(args_str).split(",") if a.strip()] - - -def _parse_kwargs(kwargs_str: str | None) -> dict[str, Any]: - if not kwargs_str or not str(kwargs_str).strip(): - return {} - try: - return json.loads(kwargs_str) - except json.JSONDecodeError: - return {} - - -def validate_workflow_graph(nodes: list[dict], edges: list[dict]) -> None: - """ - 校验画布图有效且无环。 - - 参数: - - nodes (list[dict]): 节点列表(须含 id)。 - - edges (list[dict]): 边列表(source/target)。 - - 返回: - - None - - 异常: - - ValueError: 图为空、边引用非法或存在环。 - """ - if not nodes: - raise ValueError("工作流至少需要一个节点") - ids = {n["id"] for n in nodes} - for e in edges: - if e.get("source") not in ids or e.get("target") not in ids: - raise ValueError("连线引用了不存在的节点") - in_degree: dict[str, int] = dict.fromkeys(ids, 0) - adj: dict[str, list[str]] = defaultdict(list) - for e in edges: - adj[e["source"]].append(e["target"]) - in_degree[e["target"]] += 1 - q: deque[str] = deque([nid for nid in ids if in_degree[nid] == 0]) - visited = 0 - while q: - u = q.popleft() - visited += 1 - for v in adj[u]: - in_degree[v] -= 1 - if in_degree[v] == 0: - q.append(v) - if visited != len(ids): - raise ValueError("工作流图存在环路,无法执行") - - -def _topological_levels(nodes: list[dict], edges: list[dict]) -> list[list[dict]]: - """ - 按拓扑层级分组节点:同一层级内节点互不依赖,可并行执行。 - - 参数: - - nodes (list[dict]): 节点列表。 - - edges (list[dict]): 边列表。 - - 返回: - - list[list[dict]]: 按层级分组的节点列表,顺序保证层间依赖。 - """ - id_to_node = {n["id"]: n for n in nodes} - in_degree: dict[str, int] = {n["id"]: 0 for n in nodes} - adj: dict[str, list[str]] = defaultdict(list) - for e in edges: - adj[e["source"]].append(e["target"]) - in_degree[e["target"]] += 1 - levels: list[list[dict]] = [] - current = [nid for nid in in_degree if in_degree[nid] == 0] - while current: - levels.append([id_to_node[nid] for nid in current]) - next_level: list[str] = [] - for nid in current: - for target in adj[nid]: - in_degree[target] -= 1 - if in_degree[target] == 0: - next_level.append(target) - current = next_level - return levels - - -@task(name="workflow-node", retries=0) -def prefect_node_task( - vue_node_id: str, - node_type_code: str, - code_block: str, - args_str: str | None, - kwargs_str: str | None, - upstream: dict[str, Any], - flow_variables: dict[str, Any], -) -> Any: - """ - 单个画布节点的 Prefect Task:通过 SchedulerUtil 执行用户代码块。 - - 参数: - - vue_node_id (str): 画布节点 id。 - - node_type_code (str): 节点类型编码。 - - code_block (str): 可执行代码字符串。 - - args_str (str | None): 逗号分隔位置参数说明。 - - kwargs_str (str | None): JSON 关键字参数。 - - upstream (dict[str, Any]): 上游节点输出。 - - flow_variables (dict[str, Any]): 流程级变量。 - - 返回: - - Any: 任务执行结果。 - """ - job_id = f"wfnode-{vue_node_id}" - args = _parse_args(args_str) - kw = _parse_kwargs(kwargs_str) - kw.setdefault("upstream", upstream) - kw.setdefault("variables", flow_variables) - return SchedulerUtil._task_wrapper(job_id, code_block, *args, **kw) - - -@flow(name="workflow-run", log_prints=True) -def run_workflow_prefect_flow( - ordered_nodes: list[dict], - edges: list[dict], - node_templates: dict[str, dict[str, Any]], - flow_variables: dict[str, Any], -) -> dict[str, Any]: - """ - Prefect Flow:按拓扑层级并行提交,层间串行收集结果。 - - 同层级节点互不依赖,使用 submit() 批量提交后统一收集, - 避免 .submit() → .result() 逐节点串行阻塞。 - - 参数: - - ordered_nodes (list[dict]): 已排序节点列表。 - - edges (list[dict]): 边列表。 - - node_templates (dict[str, dict[str, Any]]): 类型编码到 {func, args, kwargs}。 - - flow_variables (dict[str, Any]): 流程变量。 - - 返回: - - dict[str, Any]: 含 node_results、status 等。 - """ - levels = _topological_levels(ordered_nodes, edges) - results: dict[str, Any] = {} - for level in levels: - futures: dict[str, Any] = {} - for node in level: - nid = node["id"] - ntype = node.get("type") or "" - tpl = node_templates.get(ntype) - if not tpl or not tpl.get("func"): - raise ValueError(f"未知或未配置节点类型: {ntype}") - data = node.get("data") or {} - args_str = data.get("args") if data.get("args") is not None else tpl.get("args") - kwargs_str = data.get("kwargs") if data.get("kwargs") is not None else tpl.get("kwargs") - upstream: dict[str, Any] = {} - for e in edges: - if e.get("target") == nid and e.get("source") in results: - upstream[e["source"]] = results[e["source"]] - futures[nid] = prefect_node_task.submit( - nid, - ntype, - tpl["func"], - args_str, - kwargs_str, - upstream, - flow_variables, - ) - for nid, fut in futures.items(): - results[nid] = fut.result() - logger.info( - "Prefect workflow 完成: nodes=%s", - list(results.keys()), - ) - return { - "node_results": results, - "status": "completed", - } - - -def run_prefect_workflow_sync( - nodes: list[dict], - edges: list[dict], - node_templates: dict[str, dict[str, Any]], - flow_variables: dict[str, Any], -) -> dict[str, Any]: - """ - 同步入口:校验 DAG 后执行 Prefect Flow(Flow 内部按层级并行调度)。 - - 参数: - - nodes (list[dict]): 画布节点。 - - edges (list[dict]): 画布边。 - - node_templates (dict[str, dict[str, Any]]): 节点类型模板。 - - flow_variables (dict[str, Any]): 流程变量。 - - 返回: - - dict[str, Any]: Flow 执行汇总结果。 - """ - validate_workflow_graph(nodes, edges) - return run_workflow_prefect_flow( - ordered_nodes=nodes, - edges=edges, - node_templates=node_templates, - flow_variables=flow_variables or {}, - ) - - -def utc_now_iso() -> str: - """ - 当前 UTC 时间的 ISO 8601 字符串。 - - 返回: - - str: ISO 格式时间戳。 - """ - return datetime.now(timezone.utc).isoformat() diff --git a/backend/app/plugin/module_task/workflow/handlers/workflow_engine.py b/backend/app/plugin/module_task/workflow/handlers/workflow_engine.py new file mode 100644 index 00000000..f74534ee --- /dev/null +++ b/backend/app/plugin/module_task/workflow/handlers/workflow_engine.py @@ -0,0 +1,136 @@ +"""工作流 DAG 执行引擎 — 拓扑分层 + 并行执行""" + +from __future__ import annotations + +import json +from collections import defaultdict, deque +from concurrent.futures import ThreadPoolExecutor +from datetime import UTC, datetime +from typing import Any + +from app.core.ap_scheduler import SchedulerUtil +from app.core.logger import logger + + +def _parse_args(args_str: str | None) -> list[Any]: + if not args_str or not str(args_str).strip(): + return [] + return [a.strip() for a in str(args_str).split(",") if a.strip()] + + +def _parse_kwargs(kwargs_str: str | None) -> dict[str, Any]: + if not kwargs_str or not str(kwargs_str).strip(): + return {} + try: + return json.loads(kwargs_str) + except json.JSONDecodeError: + return {} + + +def validate_workflow_graph(nodes: list[dict], edges: list[dict]) -> None: + if not nodes: + raise ValueError("工作流至少需要一个节点") + ids = {n["id"] for n in nodes} + for e in edges: + if e.get("source") not in ids or e.get("target") not in ids: + raise ValueError("连线引用了不存在的节点") + in_degree: dict[str, int] = dict.fromkeys(ids, 0) + adj: dict[str, list[str]] = defaultdict(list) + for e in edges: + adj[e["source"]].append(e["target"]) + in_degree[e["target"]] += 1 + q: deque[str] = deque([nid for nid in ids if in_degree[nid] == 0]) + visited = 0 + while q: + u = q.popleft() + visited += 1 + for v in adj[u]: + in_degree[v] -= 1 + if in_degree[v] == 0: + q.append(v) + if visited != len(ids): + raise ValueError("工作流图存在环路,无法执行") + + +def _topological_levels(nodes: list[dict], edges: list[dict]) -> list[list[dict]]: + id_to_node = {n["id"]: n for n in nodes} + in_degree: dict[str, int] = {n["id"]: 0 for n in nodes} + adj: dict[str, list[str]] = defaultdict(list) + for e in edges: + adj[e["source"]].append(e["target"]) + in_degree[e["target"]] += 1 + levels: list[list[dict]] = [] + current = [nid for nid in in_degree if in_degree[nid] == 0] + while current: + levels.append([id_to_node[nid] for nid in current]) + next_level: list[str] = [] + for nid in current: + for target in adj[nid]: + in_degree[target] -= 1 + if in_degree[target] == 0: + next_level.append(target) + current = next_level + return levels + + +def _execute_node( + vue_node_id: str, + node_type_code: str, + code_block: str, + args_str: str | None, + kwargs_str: str | None, + upstream: dict[str, Any], + flow_variables: dict[str, Any], +) -> Any: + job_id = f"wfnode-{vue_node_id}" + args = _parse_args(args_str) + kw = _parse_kwargs(kwargs_str) + kw.setdefault("upstream", upstream) + kw.setdefault("variables", flow_variables) + return SchedulerUtil._task_wrapper(job_id, code_block, *args, **kw) + + +def run_workflow_sync( + nodes: list[dict], + edges: list[dict], + node_templates: dict[str, dict[str, Any]], + flow_variables: dict[str, Any], +) -> dict[str, Any]: + """同步执行工作流:按拓扑层级分组,同层节点并行执行。""" + validate_workflow_graph(nodes, edges) + levels = _topological_levels(nodes, edges) + results: dict[str, Any] = {} + for level in levels: + with ThreadPoolExecutor(max_workers=len(level)) as executor: + futures: dict[str, Any] = {} + for node in level: + nid = node["id"] + ntype = node.get("type") or "" + tpl = node_templates.get(ntype) + if not tpl or not tpl.get("func"): + raise ValueError(f"未知或未配置节点类型: {ntype}") + data = node.get("data") or {} + args_str = data.get("args") if data.get("args") is not None else tpl.get("args") + kwargs_str = data.get("kwargs") if data.get("kwargs") is not None else tpl.get("kwargs") + upstream: dict[str, Any] = {} + for e in edges: + if e.get("target") == nid and e.get("source") in results: + upstream[e["source"]] = results[e["source"]] + futures[nid] = executor.submit( + _execute_node, + nid, + ntype, + tpl["func"], + args_str, + kwargs_str, + upstream, + flow_variables, + ) + for nid, fut in futures.items(): + results[nid] = fut.result() + logger.info("工作流执行完成: nodes=%s", list(results.keys())) + return {"node_results": results, "status": "completed"} + + +def utc_now_iso() -> str: + return datetime.now(UTC).isoformat() diff --git a/backend/app/plugin/module_task/workflow/nodes/controller.py b/backend/app/plugin/module_task/workflow/nodes/controller.py index 1853ee4e..afe2c6cf 100644 --- a/backend/app/plugin/module_task/workflow/nodes/controller.py +++ b/backend/app/plugin/module_task/workflow/nodes/controller.py @@ -32,16 +32,8 @@ WorkflowNodeTypeRouter = APIRouter( async def get_workflow_node_type_options_controller( auth: Annotated[AuthSchema, Depends(AuthPermission(["module_task:workflow:node-type:query"]))], ) -> JSONResponse: - """ - 获取画布用编排节点类型选项(仅启用项)。 - - 参数: - - auth (AuthSchema): 认证信息。 - - 返回: - - JSONResponse: 成功响应,data 为选项列表。 - """ - result = await WorkflowNodeTypeService.get_options_service(auth=auth) + service = WorkflowNodeTypeService(auth) + result = await service.get_options() return SuccessResponse(data=result, msg="获取编排节点类型选项成功") @@ -54,17 +46,8 @@ async def get_workflow_node_type_detail_controller( id: Annotated[int, Path(description="ID")], auth: Annotated[AuthSchema, Depends(AuthPermission(["module_task:workflow:node-type:query"]))], ) -> JSONResponse: - """ - 获取编排节点类型详情。 - - 参数: - - id (int): 主键。 - - auth (AuthSchema): 认证信息。 - - 返回: - - JSONResponse: 成功响应,data 为详情。 - """ - result_dict = await WorkflowNodeTypeService.get_detail_service(auth=auth, id=id) + service = WorkflowNodeTypeService(auth) + result_dict = await service.get_detail(id=id) return SuccessResponse(data=result_dict, msg="获取编排节点类型详情成功") @@ -78,22 +61,11 @@ async def get_workflow_node_type_list_controller( search: Annotated[WorkflowNodeTypeQueryParam, Depends()], auth: Annotated[AuthSchema, Depends(AuthPermission(["module_task:workflow:node-type:query"]))], ) -> JSONResponse: - """ - 分页查询编排节点类型列表。 - - 参数: - - page (PaginationQueryParam): 分页与排序。 - - search (WorkflowNodeTypeQueryParam): 查询条件。 - - auth (AuthSchema): 认证信息。 - - 返回: - - JSONResponse: 成功响应,data 为分页结果。 - """ order_by = [{"sort_order": "asc"}, {"id": "asc"}] if page.order_by: order_by = page.order_by - result_dict = await WorkflowNodeTypeService.get_page_service( - auth=auth, + service = WorkflowNodeTypeService(auth) + result_dict = await service.get_page( page_no=page.page_no, page_size=page.page_size, search=search, @@ -111,17 +83,8 @@ async def create_workflow_node_type_controller( data: WorkflowNodeTypeCreateSchema, auth: Annotated[AuthSchema, Depends(AuthPermission(["module_task:workflow:node-type:create"]))], ) -> JSONResponse: - """ - 创建编排节点类型。 - - 参数: - - data (WorkflowNodeTypeCreateSchema): 创建体。 - - auth (AuthSchema): 认证信息。 - - 返回: - - JSONResponse: 成功响应,data 为新记录。 - """ - result_dict = await WorkflowNodeTypeService.create_service(auth=auth, data=data) + service = WorkflowNodeTypeService(auth) + result_dict = await service.create(data=data) return SuccessResponse(data=result_dict, msg="创建编排节点类型成功") @@ -135,18 +98,8 @@ async def update_workflow_node_type_controller( data: WorkflowNodeTypeUpdateSchema, auth: Annotated[AuthSchema, Depends(AuthPermission(["module_task:workflow:node-type:update"]))], ) -> JSONResponse: - """ - 更新编排节点类型。 - - 参数: - - id (int): 主键。 - - data (WorkflowNodeTypeUpdateSchema): 更新体。 - - auth (AuthSchema): 认证信息。 - - 返回: - - JSONResponse: 成功响应,data 为更新后记录。 - """ - result_dict = await WorkflowNodeTypeService.update_service(auth=auth, id=id, data=data) + service = WorkflowNodeTypeService(auth) + result_dict = await service.update(id=id, data=data) return SuccessResponse(data=result_dict, msg="更新编排节点类型成功") @@ -159,17 +112,8 @@ async def delete_workflow_node_type_controller( ids: Annotated[list[int], Body(description="ID列表")], auth: Annotated[AuthSchema, Depends(AuthPermission(["module_task:workflow:node-type:delete"]))], ) -> JSONResponse: - """ - 批量删除编排节点类型。 - - 参数: - - ids (list[int]): ID 列表。 - - auth (AuthSchema): 认证信息。 - - 返回: - - JSONResponse: 成功提示响应。 - """ - await WorkflowNodeTypeService.delete_service(auth=auth, ids=ids) + service = WorkflowNodeTypeService(auth) + await service.delete(ids=ids) return SuccessResponse(msg="删除编排节点类型成功") @@ -181,14 +125,6 @@ async def delete_workflow_node_type_controller( async def get_workflow_node_type_select_controller( auth: Annotated[AuthSchema, Depends(AuthPermission(["module_task:workflow:node-type:query"]))], ) -> JSONResponse: - """ - 获取编排节点类型选择列表。 - - 参数: - - auth (AuthSchema): 认证信息。 - - 返回: - - JSONResponse: 成功响应,data 为选择列表。 - """ - result = await WorkflowNodeTypeService.get_select_service(auth=auth) + service = WorkflowNodeTypeService(auth) + result = await service.get_select() return SuccessResponse(data=result, msg="获取编排节点类型选择列表成功") diff --git a/backend/app/plugin/module_task/workflow/nodes/model.py b/backend/app/plugin/module_task/workflow/nodes/model.py index 6f663698..283dafbb 100644 --- a/backend/app/plugin/module_task/workflow/nodes/model.py +++ b/backend/app/plugin/module_task/workflow/nodes/model.py @@ -6,7 +6,7 @@ from app.core.base_model import ModelMixin, TenantMixin, UserMixin class WorkflowNodeTypeModel(ModelMixin, TenantMixin, UserMixin): """ - 编排节点类型:用于 Vue Flow 左侧 palette 与 Prefect 运行时解析。 + 编排节点类型:用于 Vue Flow 左侧 palette 与执行引擎解析。 """ __tablename__: str = "task_workflow_node_type" diff --git a/backend/app/plugin/module_task/workflow/nodes/service.py b/backend/app/plugin/module_task/workflow/nodes/service.py index 82fb4272..a2699c18 100644 --- a/backend/app/plugin/module_task/workflow/nodes/service.py +++ b/backend/app/plugin/module_task/workflow/nodes/service.py @@ -13,22 +13,15 @@ from .schema import ( class WorkflowNodeTypeService: """工作流编排节点类型(与定时任务 task_node 无关)""" - @classmethod - def _out(cls, obj) -> WorkflowNodeTypeOutSchema: + def __init__(self, auth: AuthSchema) -> None: + self.auth = auth + + @staticmethod + def _out(obj) -> WorkflowNodeTypeOutSchema: return WorkflowNodeTypeOutSchema.model_validate(obj) - @classmethod - async def get_options_service(cls, auth: AuthSchema) -> list[dict]: - """ - 画布左侧 palette:仅返回启用项,结构与前端 Node options 对齐。 - - 参数: - - auth (AuthSchema): 认证信息。 - - 返回: - - list[dict]: 选项字典列表。 - """ - objs = await WorkflowNodeTypeCRUD(auth).list_active_options_crud() + async def get_options(self) -> list[dict]: + objs = await WorkflowNodeTypeCRUD(self.auth).list_active_options_crud() return [ { "id": o.id, @@ -41,77 +34,35 @@ class WorkflowNodeTypeService: for o in objs ] - @classmethod - async def get_detail_service(cls, auth: AuthSchema, id: int) -> WorkflowNodeTypeOutSchema: - """ - 获取编排节点类型详情。 - - 参数: - - auth (AuthSchema): 认证信息。 - - id (int): 主键。 - - 返回: - - dict: 序列化后的详情。 - - 异常: - - CustomException: 不存在时抛出。 - """ - obj = await WorkflowNodeTypeCRUD(auth).get_obj_by_id_crud(id=id) + async def get_detail(self, id: int) -> WorkflowNodeTypeOutSchema: + obj = await WorkflowNodeTypeCRUD(self.auth).get_obj_by_id_crud(id=id) if not obj: raise CustomException(msg="编排节点类型不存在") - return cls._out(obj) + return self._out(obj) - @classmethod - async def get_list_service( - cls, - auth: AuthSchema, + async def get_list( + self, search: WorkflowNodeTypeQueryParam | None = None, order_by: list[dict[str, str]] | None = None, ) -> list[WorkflowNodeTypeOutSchema]: - """ - 获取编排节点类型列表(非分页)。 - - 参数: - - auth (AuthSchema): 认证信息。 - - search (WorkflowNodeTypeQueryParam | None): 查询条件。 - - order_by (list[dict[str, str]] | None): 排序。 - - 返回: - - list[dict]: 字典列表。 - """ if order_by is None: order_by = [{"sort_order": "asc"}, {"id": "asc"}] - obj_list = await WorkflowNodeTypeCRUD(auth).get_obj_list_crud( + obj_list = await WorkflowNodeTypeCRUD(self.auth).get_obj_list_crud( search=vars(search) if search else None, order_by=order_by, ) - return [cls._out(o) for o in obj_list] + return [self._out(o) for o in obj_list] - @classmethod - async def get_page_service( - cls, - auth: AuthSchema, + async def get_page( + self, page_no: int, page_size: int, search: WorkflowNodeTypeQueryParam | None = None, order_by: list[dict[str, str]] | None = None, ) -> dict: - """ - 分页查询编排节点类型(数据库 OFFSET/LIMIT)。 - - 参数: - - auth (AuthSchema): 认证信息。 - - page_no (int): 页码。 - - page_size (int): 每页条数。 - - search (WorkflowNodeTypeQueryParam | None): 查询条件。 - - order_by (list[dict[str, str]] | None): 排序。 - - 返回: - - dict: 分页结果(items 已 JSON 友好序列化)。 - """ offset = (page_no - 1) * page_size order = order_by or [{"sort_order": "asc"}, {"id": "asc"}] - result = await WorkflowNodeTypeCRUD(auth).page( + result = await WorkflowNodeTypeCRUD(self.auth).page( offset=offset, limit=page_size, order_by=order, @@ -121,86 +72,33 @@ class WorkflowNodeTypeService: result.items = [WorkflowNodeTypeOutSchema.model_validate(item).model_dump(mode="json") for item in result.items] return result - @classmethod - async def create_service(cls, auth: AuthSchema, data: WorkflowNodeTypeCreateSchema) -> WorkflowNodeTypeOutSchema: - """ - 创建编排节点类型。 - - 参数: - - auth (AuthSchema): 认证信息。 - - data (WorkflowNodeTypeCreateSchema): 创建体。 - - 返回: - - dict: 新建记录字典。 - - 异常: - - CustomException: 编码重复或创建失败。 - """ - exist = await WorkflowNodeTypeCRUD(auth).get(code=data.code) + async def create(self, data: WorkflowNodeTypeCreateSchema) -> WorkflowNodeTypeOutSchema: + exist = await WorkflowNodeTypeCRUD(self.auth).get(code=data.code) if exist: raise CustomException(msg="节点编码已存在") - obj = await WorkflowNodeTypeCRUD(auth).create_obj_crud(data=data) + obj = await WorkflowNodeTypeCRUD(self.auth).create_obj_crud(data=data) if not obj: raise CustomException(msg="创建失败") - return cls._out(obj) + return self._out(obj) - @classmethod - async def update_service(cls, auth: AuthSchema, id: int, data: WorkflowNodeTypeUpdateSchema) -> WorkflowNodeTypeOutSchema: - """ - 更新编排节点类型。 - - 参数: - - auth (AuthSchema): 认证信息。 - - id (int): 主键。 - - data (WorkflowNodeTypeUpdateSchema): 更新体。 - - 返回: - - dict: 更新后字典。 - - 异常: - - CustomException: 不存在、编码冲突或更新失败。 - """ - exist = await WorkflowNodeTypeCRUD(auth).get_obj_by_id_crud(id=id) + async def update(self, id: int, data: WorkflowNodeTypeUpdateSchema) -> WorkflowNodeTypeOutSchema: + exist = await WorkflowNodeTypeCRUD(self.auth).get_obj_by_id_crud(id=id) if not exist: raise CustomException(msg="编排节点类型不存在") if exist.code != data.code: - other = await WorkflowNodeTypeCRUD(auth).get(code=data.code) + other = await WorkflowNodeTypeCRUD(self.auth).get(code=data.code) if other: raise CustomException(msg="节点编码已存在") - obj = await WorkflowNodeTypeCRUD(auth).update_obj_crud(id=id, data=data) + obj = await WorkflowNodeTypeCRUD(self.auth).update_obj_crud(id=id, data=data) if not obj: raise CustomException(msg="更新失败") - return cls._out(obj) + return self._out(obj) - @classmethod - async def delete_service(cls, auth: AuthSchema, ids: list[int]) -> None: - """ - 批量删除编排节点类型。 - - 参数: - - auth (AuthSchema): 认证信息。 - - ids (list[int]): ID 列表。 - - 返回: - - None - - 异常: - - CustomException: ID 为空时抛出。 - """ + async def delete(self, ids: list[int]) -> None: if not ids: raise CustomException(msg="删除ID不能为空") - await WorkflowNodeTypeCRUD(auth).delete_obj_crud(ids=ids) + await WorkflowNodeTypeCRUD(self.auth).delete_obj_crud(ids=ids) - @classmethod - async def get_select_service(cls, auth: AuthSchema) -> list[dict]: - """ - 获取编排节点类型选择列表。 - - 参数: - - auth (AuthSchema): 认证信息。 - - 返回: - - list[dict]: 选择列表,包含 id 和 name。 - """ - objs = await WorkflowNodeTypeCRUD(auth).get_obj_list_crud() + async def get_select(self) -> list[dict]: + objs = await WorkflowNodeTypeCRUD(self.auth).get_obj_list_crud() return [{"id": o.id, "name": o.name} for o in objs] diff --git a/backend/app/scripts/initialize.py b/backend/app/scripts/initialize.py index 22ebeb5e..183b863a 100644 --- a/backend/app/scripts/initialize.py +++ b/backend/app/scripts/initialize.py @@ -35,7 +35,7 @@ from app.core.database import async_db_session, create_tables from app.core.logger import logger from app.plugin.module_example.demo.model import DemoModel from app.plugin.module_task.cronjob.node.model import NodeModel -from app.plugin.module_task.workflow.node_type.model import WorkflowNodeTypeModel +from app.plugin.module_task.workflow.nodes.model import WorkflowNodeTypeModel class InitializeData: @@ -100,10 +100,6 @@ class InitializeData: async with session.begin(): await self.__init_data(session) - # ========================================================================== - # 主入口 - # ========================================================================== - async def __init_data(self, db: AsyncSession) -> None: """按依赖顺序初始化各表种子数据""" dict_type_mapping: dict[str, Any] = {} # dict_type → DictTypeModel 实例 @@ -190,10 +186,6 @@ class InitializeData: logger.error(f"❌️ 初始化 {table_name} 表数据失败") raise - # ========================================================================== - # 递归创建对象(含嵌套 children) - # ========================================================================== - @staticmethod def __create_objects_with_children(data: list[dict], model_class: type) -> list: """递归创建树形模型实例,处理嵌套 children 并注入 parent_id""" @@ -211,10 +203,6 @@ class InitializeData: return [_create(item) for item in data] - # ========================================================================== - # JSON 文件加载 - # ========================================================================== - async def __load_json(self, filename: str) -> list[dict]: """读取并解析种子数据 JSON 文件""" json_path = SCRIPT_DIR / f"{filename}.json" diff --git a/backend/app/utils/console.py b/backend/app/utils/console.py index 92a38d0b..55d7eb8b 100644 --- a/backend/app/utils/console.py +++ b/backend/app/utils/console.py @@ -10,7 +10,7 @@ from app.config.setting import settings console = get_console() -def console_run( +def console_start( host: str, port: int, reload: bool, @@ -96,7 +96,7 @@ def console_run( console.print(result) -def console_close() -> None: +def console_end() -> None: """ 在终端输出服务关闭提示面板。 diff --git a/backend/app/utils/email_util.py b/backend/app/utils/email_util.py index a4c44ded..46d94b1d 100644 --- a/backend/app/utils/email_util.py +++ b/backend/app/utils/email_util.py @@ -2,16 +2,47 @@ email_util.py — 邮件发送工具 职责: -- 渲染 Jinja2 模板 +- 渲染 Jinja2 模板(文件 & 内联字符串) - 通过 fastapi-mail 发送邮件 使用方:EmailService.send_email_service() """ +from pathlib import Path from typing import Any from fastapi_mail import ConnectionConfig, FastMail, MessageSchema, MessageType -from jinja2 import Environment, StrictUndefined +from jinja2 import Environment, FileSystemLoader, StrictUndefined + +_TEMPLATES_DIR = Path(__file__).resolve().parent.parent.parent / "templates" +_file_env: Environment | None = None + + +def _get_file_env() -> Environment: + """获取文件模板 Jinja2 环境(懒加载)。""" + global _file_env + if _file_env is None: + _file_env = Environment( + loader=FileSystemLoader(str(_TEMPLATES_DIR)), + undefined=StrictUndefined, + autoescape=True, + ) + return _file_env + + +def render_template_file(file_path: str, variables: dict[str, Any]) -> str: + """ + 从 templates/ 目录加载 Jinja2 文件模板并渲染。 + + 参数: + - file_path: 相对于 templates/ 的模板路径,如 "emails/welcome.html" + - variables: 变量字典 + + 返回:渲染后的字符串 + """ + env = _get_file_env() + tmpl = env.get_template(file_path) + return tmpl.render(**variables) def render_template(template_str: str, variables: dict[str, Any]) -> str: diff --git a/backend/app/utils/jinja2_template_util.py b/backend/app/utils/jinja2_template_util.py index fcc76681..7617391e 100644 --- a/backend/app/utils/jinja2_template_util.py +++ b/backend/app/utils/jinja2_template_util.py @@ -297,14 +297,14 @@ class Jinja2TemplateUtil: - List[str]: 模板路径列表。 """ templates = [ - "python/controller.py.j2", - "python/service.py.j2", - "python/crud.py.j2", - "python/schema.py.j2", - "python/model.py.j2", - "python/__init__.py.j2", - "ts/api.ts.j2", - "vue/index.vue.j2", + "python/controller.py.jinja2", + "python/service.py.jinja2", + "python/crud.py.jinja2", + "python/schema.py.jinja2", + "python/model.py.jinja2", + "python/__init__.py.jinja2", + "ts/api.ts.jinja2", + "vue/index.vue.jinja2", ] return templates @@ -320,9 +320,9 @@ class Jinja2TemplateUtil: - List[str]: 子表模板路径列表。 """ return [ - "python/model.py.j2", - "python/schema.py.j2", - "python/__init__.py.j2", + "python/model.py.jinja2", + "python/schema.py.jinja2", + "python/__init__.py.jinja2", ] @classmethod @@ -358,14 +358,14 @@ class Jinja2TemplateUtil: api_path = f"{frontend_api_base}/{module_name}.ts" template_mapping = { - "controller.py.j2": f"{backend_dir}/controller.py", - "service.py.j2": f"{backend_dir}/service.py", - "crud.py.j2": f"{backend_dir}/crud.py", - "schema.py.j2": f"{backend_dir}/schema.py", - "model.py.j2": f"{backend_dir}/model.py", - "__init__.py.j2": f"{backend_dir}/__init__.py", - "api.ts.j2": api_path, - "index.vue.j2": f"{view_dir}/index.vue", + "controller.py.jinja2": f"{backend_dir}/controller.py", + "service.py.jinja2": f"{backend_dir}/service.py", + "crud.py.jinja2": f"{backend_dir}/crud.py", + "schema.py.jinja2": f"{backend_dir}/schema.py", + "model.py.jinja2": f"{backend_dir}/model.py", + "__init__.py.jinja2": f"{backend_dir}/__init__.py", + "api.ts.jinja2": api_path, + "index.vue.jinja2": f"{view_dir}/index.vue", } # 查找匹配的模板路径 diff --git a/backend/app/utils/payment.py b/backend/app/utils/payment.py new file mode 100644 index 00000000..5d4093fd --- /dev/null +++ b/backend/app/utils/payment.py @@ -0,0 +1,193 @@ +"""支付网关 — 抽象基类 + 支付宝 + Mock + 工厂""" + +from __future__ import annotations + +import base64 +import json +import uuid +from abc import ABC, abstractmethod +from dataclasses import dataclass, field +from datetime import datetime +from typing import Any +from urllib.parse import urlencode + +from cryptography.hazmat.primitives import hashes, serialization +from cryptography.hazmat.primitives.asymmetric import padding, rsa + +from app.config.setting import settings +from app.core.logger import logger + +# ── dataclass ── + + +@dataclass +class PaymentInfo: + pay_url: str | None = None + qr_code_url: str | None = None + trade_no: str = "" + raw: dict[str, Any] = field(default_factory=dict) + + +@dataclass +class CallbackResult: + verified: bool = False + transaction_id: str | None = None + order_id: int | None = None + amount: int = 0 + raw: dict[str, Any] = field(default_factory=dict) + + +# ── 抽象基类 ── + + +class BasePaymentGateway(ABC): + + @abstractmethod + async def create_payment(self, order_no: str, amount: int, subject: str, notify_url: str) -> PaymentInfo: + ... + + @abstractmethod + async def verify_callback(self, data: dict[str, Any]) -> CallbackResult: + ... + + +# ── 支付宝 ── + + +class AlipayGateway(BasePaymentGateway): + GATEWAY_URL = "https://openapi.alipay.com/gateway.do" + DEV_GATEWAY_URL = "https://openapi-sandbox.dl.alipaydev.com/gateway.do" + + def __init__(self) -> None: + self.app_id = settings.PAYMENT_ALIPAY_APP_ID or "" + self._private_key = (settings.PAYMENT_ALIPAY_PRIVATE_KEY or "").encode() + self._alipay_public_key = (settings.PAYMENT_ALIPAY_PUBLIC_KEY or "").encode() + self.is_sandbox = settings.PAYMENT_ALIPAY_SANDBOX + self._gateway = self.DEV_GATEWAY_URL if self.is_sandbox else self.GATEWAY_URL + + def _sign(self, params: dict[str, Any]) -> str: + sorted_params = sorted((k, v) for k, v in params.items() if v != "" and v is not None) + sign_str = "&".join(f"{k}={v}" for k, v in sorted_params) + private_key_obj = serialization.load_pem_private_key(self._private_key, password=None) + if not isinstance(private_key_obj, rsa.RSAPrivateKey): + raise TypeError("私钥类型不是 RSA") + signature = private_key_obj.sign(sign_str.encode("utf-8"), padding.PKCS1v15(), hashes.SHA256()) + return base64.b64encode(signature).decode("utf-8") + + def _verify(self, params: dict[str, Any], signature: str) -> bool: + sorted_params = sorted( + (k, v) for k, v in params.items() + if k != "sign" and k != "sign_type" and v != "" and v is not None + ) + sign_str = "&".join(f"{k}={v}" for k, v in sorted_params) + try: + public_key_obj = serialization.load_pem_public_key(self._alipay_public_key) + if not isinstance(public_key_obj, rsa.RSAPublicKey): + return False + public_key_obj.verify(base64.b64decode(signature), sign_str.encode("utf-8"), padding.PKCS1v15(), hashes.SHA256()) + return True + except Exception: + return False + + async def create_payment(self, order_no: str, amount: int, subject: str, notify_url: str) -> PaymentInfo: + biz_content = json.dumps({ + "out_trade_no": order_no, + "total_amount": f"{amount / 100:.2f}", + "subject": subject, + "product_code": "FAST_INSTANT_TRADE_PAY", + }, ensure_ascii=False) + params = { + "app_id": self.app_id, + "method": "alipay.trade.page.pay", + "format": "JSON", + "charset": "utf-8", + "sign_type": "RSA2", + "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), + "version": "1.0", + "notify_url": notify_url, + "biz_content": biz_content, + } + params["sign"] = self._sign(params) + pay_url = f"{self._gateway}?{urlencode(params)}" + return PaymentInfo(pay_url=pay_url, trade_no="", raw=params) + + async def verify_callback(self, data: dict[str, Any]) -> CallbackResult: + sign = data.pop("sign", "") + _ = data.pop("sign_type", "") + verified = self._verify(data, sign) if sign else False + if verified and data.get("trade_status") in ("TRADE_SUCCESS", "TRADE_FINISHED"): + return CallbackResult( + verified=True, + transaction_id=data.get("trade_no"), + amount=int(float(data.get("total_amount", 0)) * 100), + raw=data, + ) + return CallbackResult(verified=False, raw=data) + + +# ── Mock ── + + +class MockPaymentGateway(BasePaymentGateway): + + def __init__(self) -> None: + self._pending_orders: dict[str, dict] = {} + + async def create_payment(self, order_no: str, amount: int, subject: str, notify_url: str) -> PaymentInfo: + trade_no = f"MOCK{uuid.uuid4().hex[:16].upper()}" + self._pending_orders[order_no] = {"trade_no": trade_no, "amount": amount, "subject": subject} + pay_url = f"/mock-pay?order_no={order_no}&amount={amount}" + qr_url = f"https://api.qrserver.com/v1/create-qr-code/?size=200x200&data={pay_url}" + return PaymentInfo(pay_url=pay_url, qr_code_url=qr_url, trade_no=trade_no, raw={"order_no": order_no, "amount": amount, "is_mock": True}) + + async def verify_callback(self, data: dict[str, Any]) -> CallbackResult: + order_no = data.get("order_no", "") + pending = self._pending_orders.pop(order_no, None) + if pending: + return CallbackResult(verified=True, transaction_id=pending["trade_no"], order_id=data.get("order_id"), amount=pending["amount"], raw={**data, **pending}) + return CallbackResult(verified=False, raw=data) + + def get_mock_callback_data(self, order_id: int, order_no: str) -> dict: + pending = self._pending_orders.get(order_no, {}) + return { + "order_id": order_id, + "order_no": order_no, + "trade_no": pending.get("trade_no", f"MOCK{uuid.uuid4().hex[:16].upper()}"), + "total_amount": str(pending.get("amount", 0) / 100), + "trade_status": "TRADE_SUCCESS", + } + + +# ── 工厂 ── + +_mock_gateway: MockPaymentGateway | None = None + + +def create_payment_gateway(method: str = "") -> BasePaymentGateway: + if method == "alipay": + logger.info("🔔 使用支付宝支付网关") + return AlipayGateway() + if method == "wxpay": + logger.info("🔔 使用微信支付网关(未实现,回退 Mock)") + return MockPaymentGateway() + if settings.PAYMENT_ALIPAY_APP_ID and settings.PAYMENT_ALIPAY_PRIVATE_KEY: + logger.info("🔔 自动选择支付宝支付网关") + return AlipayGateway() + logger.info("🔔 使用 Mock 支付网关(开发模式)") + return MockPaymentGateway() + + +def get_mock_gateway() -> MockPaymentGateway: + global _mock_gateway + if _mock_gateway is None: + _mock_gateway = MockPaymentGateway() + return _mock_gateway + + +__all__ = [ + "BasePaymentGateway", + "PaymentInfo", + "CallbackResult", + "create_payment_gateway", + "get_mock_gateway", +] diff --git a/backend/main.py b/backend/main.py index 3afd822b..f8503dd5 100755 --- a/backend/main.py +++ b/backend/main.py @@ -23,17 +23,10 @@ def create_app() -> FastAPI: 返回: - FastAPI: 已配置生命周期的应用对象。 """ - from app.init_app import ( - lifespan, - register_exceptions, - register_files, - register_middlewares, - register_routers, - reset_api_docs, - ) + from app.init_app import lifespan, register_exceptions, register_files, register_middlewares, register_routers, reset_api_docs # 创建FastAPI应用 app = FastAPI(**settings.FASTAPI_CONFIG, lifespan=lifespan) - # 注册各种组件 + # 注册异常处理器 register_exceptions(app) # 注册中间件 register_middlewares(app) diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 8b215a4e..ac5183e5 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -3,7 +3,7 @@ name = "backend" version = "2.0.0" description = "fastapiadmin后端工程" readme = "README.md" -requires-python = ">=3.10" +requires-python = ">=3.12" dependencies = [ "agno==2.5.8", "aiofiles==24.1.0", # 文件操作 @@ -16,12 +16,11 @@ dependencies = [ "bcrypt==4.0.1", # 密码加密解析,切勿升级,如果升级,请同时升级python版本 "bleach==6.3.0", # 过滤 HTML 标签 "click==8.1.7", # 命令行参数解析 - "croniter>=1.0.12,<5.0.0", # cron(与 prefect 约束一致;部署 requirements.txt 同源) + "croniter>=1.0.12,<5.0.0", # cron 表达式解析 "cryptography==45.0.2", # mysql8 密码加密 "fastapi==0.115.2", # fastapi 框架 "fastapi-limiter==0.1.6", # 接口限流 - "greenlet==3.1.1", # 协程框架 - "gunicorn==23.0.0", # 协程框架 + "greenlet==3.1.1", # 协程核心依赖(SQLAlchemy 异步) "httpx==0.27.2", # HTTP 客户端 "itsdangerous==2.2.0", # 用于安全处理各种数据,如密码、密钥等 "jinja2==3.1.6", # 模板引擎 @@ -30,7 +29,6 @@ dependencies = [ "openpyxl==3.1.5", # Excel "pandas==2.2.3", # 数据处理 "passlib==1.7.4", # 密码加密 - "prefect>=3.5.0,<4", # 工作流编排(与 typer>=0.19 兼容;部署与 requirements.txt 一致) "pillow==11.0.0", # 图片处理 "psutil==6.1.0", # 系统信息 "psycopg==3.3.2", # postgresql 同步操作数据库基于 psycopg是psycopg2升级版:psycopg2 是一个 pure-Python PostgreSQL 适配器。 @@ -42,18 +40,16 @@ dependencies = [ "python-multipart==0.0.9", # request.form() 对表单进行「解析」时安装 "redis==7.1.0", # redis 同/异步操作数据库(用户celery配套使用)redis 异步操作数据库 redis已经完全具备了aioredis的功能,无需重复安全,且aioredis已经不再维护也不兼容3.10+的版本 "rich==13.9.4", # 终端打印美化 - "tomli>=2.0.1; python_version < '3.11'", # 解析 plugin.toml(3.11+ 用标准库 tomllib) "sqlalchemy==2.0.45", # 数据库ORM "sqlglot[rs]==27.8.0", # sql 解析 - "structlog==25.5.0", # 结构化日志 "typer==0.19.0", # 命令行工具 "user-agents==2.2.0", # 获取用户UA "uvicorn==0.30.6", # uvicorn web 框架 - "websockets>=15.0.1,<16.0", # websocket(prefect>=3.5 需 >=15.0.1;与部署 requirements.txt 一致) - "tomli>=2.0.1 ; python_full_version < '3.15'", + "websockets>=15.0.1,<16.0", # websocket 通信 "fastapi-cache2[redis]>=0.1.8", "fastapi-mail==1.5.1", # 邮件发送(简化 EmailSendService 底层) "tinycss2==1.5.1", + "weasyprint==63.1", ] [dependency-groups] diff --git a/backend/requirements.txt b/backend/requirements.txt index 102f8d9c..e9caddce 100755 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -1,7 +1,7 @@ # 与 pyproject.toml [project] dependencies 一致(生产 pip;不含 dev 组:pytest / ruff / fakeredis) agno==2.5.8 # AI agent框架 aiofiles==24.1.0 # 文件操作 -aiosmtplib>=3.0.2 # 异步 SMTP 邮件发送(fastapi-mail 要求 >=3.0.2) +aiosmtplib>=3.0.2 # 异步 SMTP 邮件发送(fastapi-mail 要求 >=3.0.2) aiosqlite==0.17.0 # sqlite 异步操作数据库 alembic==1.15.1 # 数据库迁移 apscheduler==3.11.0 # 定时任务 @@ -11,14 +11,13 @@ bcrypt==4.0.1 # 密码加密解析,切勿升级,如 bleach==6.3.0 # 过滤 HTML 标签 tinycss2==1.5.1 # bleach CSS 清洗依赖 click==8.1.7 # 命令行参数解析 -croniter>=1.0.12,<5.0.0 # cron(与 prefect 约束一致;部署 requirements.txt 同源) +croniter>=1.0.12,<5.0.0 # cron 表达式解析 cryptography==45.0.2 # mysql8 密码加密 fastapi==0.115.2 # fastapi 框架 fastapi-limiter==0.1.6 # 接口限流 -fastapi-cache2[redis]>=0.2.2 # 缓存 +fastapi-cache2[redis]>=0.1.8 # 缓存 fastapi-mail==1.5.1 # 邮件发送(替换手写 aiosmtplib 逻辑) -greenlet==3.1.1 # 协程框架 -gunicorn==23.0.0 # 协程框架 +greenlet==3.1.1 # 协程核心依赖(SQLAlchemy 异步) httpx==0.27.2 # HTTP 客户端 itsdangerous==2.2.0 # 用于安全处理各种数据,如密码、密钥等 jinja2==3.1.6 # 模板引擎 @@ -27,7 +26,6 @@ openai==2.28.0 # OpenAI API 客户端 openpyxl==3.1.5 # Excel pandas==2.2.3 # 数据处理 passlib==1.7.4 # 密码加密 -prefect>=3.5.0,<4 # 工作流编排(与 typer>=0.19 兼容;部署与 requirements.txt 一致) pillow==11.0.0 # 图片处理 psutil==6.1.0 # 系统信息 psycopg==3.3.2 # PostgreSQL 异步操作数据库 @@ -39,12 +37,10 @@ pymysql==1.1.2 # MySQL 异步操作数据库 python-multipart==0.0.9 # request.form() 对表单进行「解析」时安装 redis==7.1.0 # Redis 异步操作数据库 rich==13.9.4 # 终端打印美化 -tomli>=2.0.1; python_version < "3.15" # 解析 plugin.toml(3.11+ 用标准库 tomllib) sqlalchemy==2.0.45 # 数据库ORM sqlglot[rs]==27.8.0 # sql 解析 -structlog==25.5.0 # 结构化日志 typer==0.19.0 # 命令行工具 user-agents==2.2.0 # 获取用户UA uvicorn==0.30.6 # uvicorn web 框架 -websockets>=15.0.1,<16.0 # websocket(prefect>=3.5 需 >=15.0.1;与部署 requirements.txt 一致) -weasyprint==63.1 # HTML/CSS → PDF 渲染(电子发票 PDF 生成) +websockets>=15.0.1,<16.0 # websocket 通信 +weasyprint==63.1 # HTML/CSS → PDF 渲染(电子发票 PDF 生成) diff --git a/backend/templates/emails/reset_password.jinja2 b/backend/templates/emails/reset_password.jinja2 new file mode 100644 index 00000000..7eb7e5ae --- /dev/null +++ b/backend/templates/emails/reset_password.jinja2 @@ -0,0 +1,31 @@ + + + + + + + +
+
密码重置
+
+

{{ username }},您好!

+

请点击下方按钮重置密码({{ expire_minutes }} 分钟内有效):

+ 重置密码 +

如果按钮无法点击,请复制以下链接到浏览器打开:

+

{{ reset_url }}

+

如果非您本人操作,请忽略此邮件。

+
+ +
+ + diff --git a/backend/templates/emails/welcome.jinja2 b/backend/templates/emails/welcome.jinja2 new file mode 100644 index 00000000..dfebaa9c --- /dev/null +++ b/backend/templates/emails/welcome.jinja2 @@ -0,0 +1,27 @@ + + + + + + + +
+
欢迎加入 {{ tenant_name }}!
+
+

{{ username }},您好!

+

您的租户已成功创建,试用期至 {{ trial_end }}

+

请登录后台开始使用。

+
+ +
+ + diff --git a/backend/templates/invoice.html.j2 b/backend/templates/invoice.jinja2 similarity index 100% rename from backend/templates/invoice.html.j2 rename to backend/templates/invoice.jinja2 diff --git a/backend/templates/python/__init__.py.j2 b/backend/templates/python/__init__.py.jinja2 similarity index 100% rename from backend/templates/python/__init__.py.j2 rename to backend/templates/python/__init__.py.jinja2 diff --git a/backend/templates/python/controller.py.j2 b/backend/templates/python/controller.py.jinja2 similarity index 100% rename from backend/templates/python/controller.py.j2 rename to backend/templates/python/controller.py.jinja2 diff --git a/backend/templates/python/crud.py.j2 b/backend/templates/python/crud.py.jinja2 similarity index 100% rename from backend/templates/python/crud.py.j2 rename to backend/templates/python/crud.py.jinja2 diff --git a/backend/templates/python/model.py.j2 b/backend/templates/python/model.py.jinja2 similarity index 100% rename from backend/templates/python/model.py.j2 rename to backend/templates/python/model.py.jinja2 diff --git a/backend/templates/python/schema.py.j2 b/backend/templates/python/schema.py.jinja2 similarity index 100% rename from backend/templates/python/schema.py.j2 rename to backend/templates/python/schema.py.jinja2 diff --git a/backend/templates/python/service.py.j2 b/backend/templates/python/service.py.jinja2 similarity index 100% rename from backend/templates/python/service.py.j2 rename to backend/templates/python/service.py.jinja2 diff --git a/backend/templates/ts/api.ts.j2 b/backend/templates/ts/api.ts.jinja2 similarity index 100% rename from backend/templates/ts/api.ts.j2 rename to backend/templates/ts/api.ts.jinja2 diff --git a/backend/templates/vue/index.vue.j2 b/backend/templates/vue/index.vue.jinja2 similarity index 100% rename from backend/templates/vue/index.vue.j2 rename to backend/templates/vue/index.vue.jinja2 diff --git a/backend/tests/test_system_modules.py b/backend/tests/test_system_modules.py index 0ce679fa..486018c5 100644 --- a/backend/tests/test_system_modules.py +++ b/backend/tests/test_system_modules.py @@ -29,8 +29,7 @@ def fake_auth() -> AuthSchema: return AuthSchema( db=mock_db, user=fake_user, - is_super_admin=True, - check_data_scope=False, + tenant_id=fake_user.tenant_id, ) @@ -291,7 +290,8 @@ class TestAuthModule: from app.core.base_schema import AuthSchema assert "user" in AuthSchema.model_fields - assert "check_data_scope" in AuthSchema.model_fields + assert "db" in AuthSchema.model_fields + assert "tenant_id" in AuthSchema.model_fields def test_login_flow_mock(self) -> None: """验证登录相关 schema 逻辑""" diff --git a/backend/tests/test_unit.py b/backend/tests/test_unit.py index 7398a25f..257419f6 100644 --- a/backend/tests/test_unit.py +++ b/backend/tests/test_unit.py @@ -9,22 +9,15 @@ class TestTenantContextVar: """租户上下文变量读写与清理。""" def test_initially_none(self) -> None: - from app.core.tenant import get_current_tenant_id, get_is_super_admin + from app.core.request_context import get_current_tenant_id assert get_current_tenant_id() is None - assert get_is_super_admin() is False def test_set_and_get(self) -> None: - from app.core.tenant import ( - clear_current_tenant, - get_current_tenant_id, - get_is_super_admin, - set_current_tenant, - ) + from app.core.request_context import clear_current_tenant, get_current_tenant_id, set_current_tenant - set_current_tenant(42, True) + set_current_tenant(42) assert get_current_tenant_id() == 42 - assert get_is_super_admin() is True clear_current_tenant() assert get_current_tenant_id() is None diff --git a/backend/uv.lock b/backend/uv.lock index b49cae9a..abebdd2f 100644 --- a/backend/uv.lock +++ b/backend/uv.lock @@ -1,11 +1,9 @@ version = 1 revision = 3 -requires-python = ">=3.10" +requires-python = ">=3.12" resolution-markers = [ "python_full_version >= '3.13'", - "python_full_version == '3.12.*'", - "python_full_version == '3.11.*'", - "python_full_version < '3.11'", + "python_full_version < '3.13'", ] [[package]] @@ -89,15 +87,6 @@ wheels = [ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/99/f7/d398fae160568472ddce0b3fde9c4581afc593019a6adc91006a66406991/alembic-1.15.1-py3-none-any.whl", hash = "sha256:197de710da4b3e91cf66a826a5b31b5d59a127ab41bd0fc42863e2902ce2bbbe", size = 231753, upload-time = "2025-03-04T22:02:41.673Z" }, ] -[[package]] -name = "amplitude-analytics" -version = "1.2.2" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/3c/f3/92e68da6feca11b8d279da0b8f413d35395eef12d243d0c6826924e6f04d/amplitude_analytics-1.2.2.tar.gz", hash = "sha256:ed82b39aa04447e5f156398249d632a5b51591905ec16556c7641a3258d38366", size = 22262, upload-time = "2026-02-17T17:17:09.637Z" } -wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/bf/c0/1486294b558353b0ee91cac7e876b594b61824e19fcbd4c3f9e7f749a3b8/amplitude_analytics-1.2.2-py3-none-any.whl", hash = "sha256:18f1b6e03cf360fcbea4bffb3a44883f063039ea1622fc85b0da945bdd13f6c6", size = 24660, upload-time = "2026-02-17T17:17:08.8Z" }, -] - [[package]] name = "annotated-types" version = "0.7.0" @@ -112,7 +101,6 @@ name = "anyio" version = "4.12.1" source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } dependencies = [ - { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "idna" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] @@ -121,24 +109,6 @@ wheels = [ { url = "https://pypi.tuna.tsinghua.edu.cn/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 = "apprise" -version = "1.9.9" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } -dependencies = [ - { name = "certifi" }, - { name = "click" }, - { name = "markdown" }, - { name = "pyyaml" }, - { name = "requests" }, - { name = "requests-oauthlib" }, - { name = "tzdata", marker = "sys_platform == 'win32'" }, -] -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/20/f4/be5c7e39b83a2285ab62ae7c19bb10704836f59c0a5b4c471730f54c9f98/apprise-1.9.9.tar.gz", hash = "sha256:fd622c0df16bdc79ed385539735573488cafe2405d25747e87eebd6b09b26012", size = 2032822, upload-time = "2026-03-21T17:49:14.041Z" } -wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e6/2f/54d068d7e011a8b4e0aae3e93b09a30b33bcf780829fe70c6e8876aeb0e0/apprise-1.9.9-py3-none-any.whl", hash = "sha256:55ceb8827a1c783d683881c9f77fa42eb43b3fc91b854419c452d557101c7068", size = 1519940, upload-time = "2026-03-21T17:49:11.847Z" }, -] - [[package]] name = "apscheduler" version = "3.11.0" @@ -151,18 +121,6 @@ wheels = [ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d0/ae/9a053dd9229c0fde6b1f1f33f609ccff1ee79ddda364c756a924c6d8563b/APScheduler-3.11.0-py3-none-any.whl", hash = "sha256:fc134ca32e50f5eadcc4938e3a4545ab19131435e851abb40b34d63d5141c6da", size = 64004, upload-time = "2024-11-24T19:39:24.442Z" }, ] -[[package]] -name = "asgi-lifespan" -version = "2.1.0" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } -dependencies = [ - { name = "sniffio" }, -] -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/6a/da/e7908b54e0f8043725a990bf625f2041ecf6bfe8eb7b19407f1c00b630f7/asgi-lifespan-2.1.0.tar.gz", hash = "sha256:5e2effaf0bfe39829cf2d64e7ecc47c7d86d676a6599f7afba378c31f5e3a308", size = 15627, upload-time = "2023-03-28T17:35:49.126Z" } -wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/2f/f5/c36551e93acba41a59939ae6a0fb77ddb3f2e8e8caa716410c65f7341f72/asgi_lifespan-2.1.0-py3-none-any.whl", hash = "sha256:ed840706680e28428c01e14afb3875d7d76d3206f3d5b2f2294e059b5c23804f", size = 10895, upload-time = "2023-03-28T17:35:47.772Z" }, -] - [[package]] name = "async-timeout" version = "5.0.1" @@ -178,22 +136,6 @@ version = "0.2.11" source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/78/3c/d8297584c40f3d1af55365026bcdca7844ecfea1d917ad19df48f8331a26/asyncmy-0.2.11.tar.gz", hash = "sha256:c3d65d959dde62c911e39ecd1ad0f1339a5e6929fc411d48cfc2f82846190bf4", size = 62865, upload-time = "2026-01-15T11:32:30.368Z" } wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c3/35/9612472ac9722d9be9517ee02bcffa623a1888a5fdd7e69b8c007d98d7e7/asyncmy-0.2.11-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d1c3b957024d1eccb5053e78aa6e1e522943922a4853b03402acf13c93e9e54d", size = 1759953, upload-time = "2026-01-15T11:31:28.223Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d8/be/63fdc4594f48083fe9319e28533fa8a9374a15a65014d7c4a3abe11b5ca4/asyncmy-0.2.11-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d6fc770fd29784302b4961e0ce865712e88aabc8451d42381193fa8f2b2a92ad", size = 1726944, upload-time = "2026-01-15T11:31:30.124Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ba/13/608c1bfa94ae149ba32f68b478d6a8dcf7b3ee84dbb91f9c04129c43a6e6/asyncmy-0.2.11-cp310-cp310-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:dc88df5765058638a12f0bf6e4f5ef00d9f31f7297b60cd283313eadaa5a472b", size = 4866928, upload-time = "2026-01-15T11:31:32.81Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/35/00/a4e1c5dc2fa1d6ee20a01c06480fcf20569ed2a5b73bd7a19606727a9f10/asyncmy-0.2.11-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efe78f5b498499e3b1730a0f1d3fdd98514bb4c5fd07eaf87e21f9078086ece2", size = 5053411, upload-time = "2026-01-15T11:31:34.672Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/59/66/581e610303746bade5e766a5de2edc958e2a066dfc55a432db8d0bede7d3/asyncmy-0.2.11-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5ed33eb733f5f22cb5245a8186d45dc3ce5cf6f0d71b3c0d0d05944b0fe4d495", size = 4895574, upload-time = "2026-01-15T11:31:36.255Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/89/e8/3af264bd42f73d8c8794bb1372fb985595edd05bdbd3271ea1a9f8e99f52/asyncmy-0.2.11-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:549477362902aa7698f03a3f18fe07f8dd5e04eb97ccddd26949ba67b4b7c4df", size = 5007339, upload-time = "2026-01-15T11:31:38.226Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/62/c1/7f33704f680a4b3d1b630a4621d80a06d17bf46c0084fd169288844119fe/asyncmy-0.2.11-cp310-cp310-win32.whl", hash = "sha256:52e86c354d43da07dc924635874a1edd0824e97355d46d75532b29ee87559412", size = 1584808, upload-time = "2026-01-15T11:31:39.517Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/88/ef/a78244b5e293ef2e4fca1b66dc3c3f65d73aa035531800bf6d1d37a87667/asyncmy-0.2.11-cp310-cp310-win_amd64.whl", hash = "sha256:76ff057608aa78bba5c8e6a5b0fd373cd4759fcbd31f1ea4d984c925ef84f575", size = 1653423, upload-time = "2026-01-15T11:31:40.889Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/cb/58/ec29057913334cacaa2be98eacb21ad468bc58214ff25aab28b78487d697/asyncmy-0.2.11-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6b324b70438120cabdb1b0d8789082b8a2770257e7beaa7d036322ba5f603f25", size = 1757174, upload-time = "2026-01-15T11:31:43.006Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/7e/46/7939167e05143c8feba1812dbdd478579d215d4b6d7f84856981ab71db7c/asyncmy-0.2.11-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:919811ec4157506632a354047b943b995c71f31182d430ab87c60ccee844c1d9", size = 1724357, upload-time = "2026-01-15T11:31:44.826Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/1f/a0/5cfc1bdeb6166ea26eac97d3cefb574d490aabc052478a3f65730b7379d9/asyncmy-0.2.11-cp311-cp311-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:8f416fba944dad20d47eb9a4db1a1853a0f7856b12b0a3b52c1a2fba2f447851", size = 5100925, upload-time = "2026-01-15T11:31:46.842Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/0e/37/d9bda68844e6741a7e7e1e449da6cc7926b7ccbf11af26a806b19fd7e7ae/asyncmy-0.2.11-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:905a6693e3dbaf4280dfced96131bada108554754df40d75a7ff91961ba7331a", size = 5303964, upload-time = "2026-01-15T11:31:48.719Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/57/da/358b6e50148a808e05963acd2ae47c1fbef4cdead0157ce2fc90fd4ecf7d/asyncmy-0.2.11-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ff3ac9feb09e4e80c227280f6350c8015cfed018efe9090f41e2fb4ab8cbf163", size = 5116361, upload-time = "2026-01-15T11:31:50.26Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e9/e8/6b11d62b0e164adbff5ed882d650c8b60fa710001965c54c70c77cffc941/asyncmy-0.2.11-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:be18725eeccb616b5769ed8cbcabb2758a47e162c4232d7bef98b978950228ad", size = 5241238, upload-time = "2026-01-15T11:31:51.708Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/34/6b/d28fe93160900aba9573cbbf9be5291ac2f1a37eab4fd85e366d5ecbfd84/asyncmy-0.2.11-cp311-cp311-win32.whl", hash = "sha256:d32e03278b72f7ef80c0692797e0ce1c03a63ac0192f858d804a6ecd471754e3", size = 1583714, upload-time = "2026-01-15T11:31:52.968Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/09/29/39effc0c56fea76b695490b95df0da4dd0b6f040ed160ad40740e1358265/asyncmy-0.2.11-cp311-cp311-win_amd64.whl", hash = "sha256:adfe7d271f9ef52c5f96ffd716ed7726839ec7c39fccf98748803f871b399dbd", size = 1655038, upload-time = "2026-01-15T11:31:54.161Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ca/93/3b4c7f9b35a27e80bd2f305c4c8d7ae56b6dd40d616d34dd4dbb818c90c9/asyncmy-0.2.11-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:914dddd2ba884822a304297f6ca96026548a100339a4e0ca5c427f6cfa3e4b62", size = 1731817, upload-time = "2026-01-15T11:31:55.486Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/7b/65/c70b2b8d014b21504de7e2027e2456f7774cec855766ec1808da47d70b24/asyncmy-0.2.11-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5ea7ca4bcf7747b1fd4e80f16aa461972e0931cecd3bc43a3ba8e14a5d368a98", size = 1712017, upload-time = "2026-01-15T11:31:56.795Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/65/a9/f326999a1ffacc7738376fa68c7ede164db9e5520bb4dbd35f1fdd5704dd/asyncmy-0.2.11-cp312-cp312-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:b2fd6e04efca56d9176e7b620ee47dd1dd334e2eb03c1ae1580954c2625b99d1", size = 4986605, upload-time = "2026-01-15T11:31:58.462Z" }, @@ -216,27 +158,8 @@ wheels = [ name = "asyncpg" version = "0.30.0" source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } -dependencies = [ - { name = "async-timeout", marker = "python_full_version < '3.11'" }, -] sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/2f/4c/7c991e080e106d854809030d8584e15b2e996e26f16aee6d757e387bc17d/asyncpg-0.30.0.tar.gz", hash = "sha256:c551e9928ab6707602f44811817f82ba3c446e018bfe1d3abecc8ba5f3eac851", size = 957746, upload-time = "2024-10-20T00:30:41.127Z" } wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/bb/07/1650a8c30e3a5c625478fa8aafd89a8dd7d85999bf7169b16f54973ebf2c/asyncpg-0.30.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bfb4dd5ae0699bad2b233672c8fc5ccbd9ad24b89afded02341786887e37927e", size = 673143, upload-time = "2024-10-20T00:29:08.846Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a0/9a/568ff9b590d0954553c56806766914c149609b828c426c5118d4869111d3/asyncpg-0.30.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dc1f62c792752a49f88b7e6f774c26077091b44caceb1983509edc18a2222ec0", size = 645035, upload-time = "2024-10-20T00:29:12.02Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/de/11/6f2fa6c902f341ca10403743701ea952bca896fc5b07cc1f4705d2bb0593/asyncpg-0.30.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3152fef2e265c9c24eec4ee3d22b4f4d2703d30614b0b6753e9ed4115c8a146f", size = 2912384, upload-time = "2024-10-20T00:29:13.644Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/83/83/44bd393919c504ffe4a82d0aed8ea0e55eb1571a1dea6a4922b723f0a03b/asyncpg-0.30.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c7255812ac85099a0e1ffb81b10dc477b9973345793776b128a23e60148dd1af", size = 2947526, upload-time = "2024-10-20T00:29:15.871Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/08/85/e23dd3a2b55536eb0ded80c457b0693352262dc70426ef4d4a6fc994fa51/asyncpg-0.30.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:578445f09f45d1ad7abddbff2a3c7f7c291738fdae0abffbeb737d3fc3ab8b75", size = 2895390, upload-time = "2024-10-20T00:29:19.346Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/9b/26/fa96c8f4877d47dc6c1864fef5500b446522365da3d3d0ee89a5cce71a3f/asyncpg-0.30.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c42f6bb65a277ce4d93f3fba46b91a265631c8df7250592dd4f11f8b0152150f", size = 3015630, upload-time = "2024-10-20T00:29:21.186Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/34/00/814514eb9287614188a5179a8b6e588a3611ca47d41937af0f3a844b1b4b/asyncpg-0.30.0-cp310-cp310-win32.whl", hash = "sha256:aa403147d3e07a267ada2ae34dfc9324e67ccc4cdca35261c8c22792ba2b10cf", size = 568760, upload-time = "2024-10-20T00:29:22.769Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f0/28/869a7a279400f8b06dd237266fdd7220bc5f7c975348fea5d1e6909588e9/asyncpg-0.30.0-cp310-cp310-win_amd64.whl", hash = "sha256:fb622c94db4e13137c4c7f98834185049cc50ee01d8f657ef898b6407c7b9c50", size = 625764, upload-time = "2024-10-20T00:29:25.882Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/4c/0e/f5d708add0d0b97446c402db7e8dd4c4183c13edaabe8a8500b411e7b495/asyncpg-0.30.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5e0511ad3dec5f6b4f7a9e063591d407eee66b88c14e2ea636f187da1dcfff6a", size = 674506, upload-time = "2024-10-20T00:29:27.988Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/6a/a0/67ec9a75cb24a1d99f97b8437c8d56da40e6f6bd23b04e2f4ea5d5ad82ac/asyncpg-0.30.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:915aeb9f79316b43c3207363af12d0e6fd10776641a7de8a01212afd95bdf0ed", size = 645922, upload-time = "2024-10-20T00:29:29.391Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/5c/d9/a7584f24174bd86ff1053b14bb841f9e714380c672f61c906eb01d8ec433/asyncpg-0.30.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c198a00cce9506fcd0bf219a799f38ac7a237745e1d27f0e1f66d3707c84a5a", size = 3079565, upload-time = "2024-10-20T00:29:30.832Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a0/d7/a4c0f9660e333114bdb04d1a9ac70db690dd4ae003f34f691139a5cbdae3/asyncpg-0.30.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3326e6d7381799e9735ca2ec9fd7be4d5fef5dcbc3cb555d8a463d8460607956", size = 3109962, upload-time = "2024-10-20T00:29:33.114Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/3c/21/199fd16b5a981b1575923cbb5d9cf916fdc936b377e0423099f209e7e73d/asyncpg-0.30.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:51da377487e249e35bd0859661f6ee2b81db11ad1f4fc036194bc9cb2ead5056", size = 3064791, upload-time = "2024-10-20T00:29:34.677Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/77/52/0004809b3427534a0c9139c08c87b515f1c77a8376a50ae29f001e53962f/asyncpg-0.30.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bc6d84136f9c4d24d358f3b02be4b6ba358abd09f80737d1ac7c444f36108454", size = 3188696, upload-time = "2024-10-20T00:29:36.389Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/52/cb/fbad941cd466117be58b774a3f1cc9ecc659af625f028b163b1e646a55fe/asyncpg-0.30.0-cp311-cp311-win32.whl", hash = "sha256:574156480df14f64c2d76450a3f3aaaf26105869cad3865041156b38459e935d", size = 567358, upload-time = "2024-10-20T00:29:37.915Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/3c/0a/0a32307cf166d50e1ad120d9b81a33a948a1a5463ebfa5a96cc5606c0863/asyncpg-0.30.0-cp311-cp311-win_amd64.whl", hash = "sha256:3356637f0bd830407b5597317b3cb3571387ae52ddc3bca6233682be88bbbc1f", size = 629375, upload-time = "2024-10-20T00:29:39.987Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/4b/64/9d3e887bb7b01535fdbc45fbd5f0a8447539833b97ee69ecdbb7a79d0cb4/asyncpg-0.30.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c902a60b52e506d38d7e80e0dd5399f657220f24635fee368117b8b5fce1142e", size = 673162, upload-time = "2024-10-20T00:29:41.88Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/6e/eb/8b236663f06984f212a087b3e849731f917ab80f84450e943900e8ca4052/asyncpg-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:aca1548e43bbb9f0f627a04666fedaca23db0a31a84136ad1f868cb15deb6e3a", size = 637025, upload-time = "2024-10-20T00:29:43.352Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/cc/57/2dc240bb263d58786cfaa60920779af6e8d32da63ab9ffc09f8312bd7a14/asyncpg-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c2a2ef565400234a633da0eafdce27e843836256d40705d83ab7ec42074efb3", size = 3496243, upload-time = "2024-10-20T00:29:44.922Z" }, @@ -255,15 +178,6 @@ wheels = [ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c8/a4/cec76b3389c4c5ff66301cd100fe88c318563ec8a520e0b2e792b5b84972/asyncpg-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:f59b430b8e27557c3fb9869222559f7417ced18688375825f8f12302c34e915e", size = 621623, upload-time = "2024-10-20T00:30:09.024Z" }, ] -[[package]] -name = "attrs" -version = "25.4.0" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251, upload-time = "2025-10-06T13:54:44.725Z" } -wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, -] - [[package]] name = "backend" version = "2.0.0" @@ -287,7 +201,6 @@ dependencies = [ { name = "fastapi-limiter" }, { name = "fastapi-mail" }, { name = "greenlet" }, - { name = "gunicorn" }, { name = "httpx" }, { name = "itsdangerous" }, { name = "jinja2" }, @@ -297,7 +210,6 @@ dependencies = [ { name = "pandas" }, { name = "passlib" }, { name = "pillow" }, - { name = "prefect" }, { name = "psutil" }, { name = "psycopg" }, { name = "psycopg-binary" }, @@ -310,12 +222,11 @@ dependencies = [ { name = "rich" }, { name = "sqlalchemy" }, { name = "sqlglot", extra = ["rs"] }, - { name = "structlog" }, { name = "tinycss2" }, - { name = "tomli", marker = "python_full_version < '3.15'" }, { name = "typer" }, { name = "user-agents" }, { name = "uvicorn" }, + { name = "weasyprint" }, { name = "websockets" }, ] @@ -347,7 +258,6 @@ requires-dist = [ { name = "fastapi-limiter", specifier = "==0.1.6" }, { name = "fastapi-mail", specifier = "==1.5.1" }, { name = "greenlet", specifier = "==3.1.1" }, - { name = "gunicorn", specifier = "==23.0.0" }, { name = "httpx", specifier = "==0.27.2" }, { name = "itsdangerous", specifier = "==2.2.0" }, { name = "jinja2", specifier = "==3.1.6" }, @@ -357,7 +267,6 @@ requires-dist = [ { name = "pandas", specifier = "==2.2.3" }, { name = "passlib", specifier = "==1.7.4" }, { name = "pillow", specifier = "==11.0.0" }, - { name = "prefect", specifier = ">=3.5.0,<4" }, { name = "psutil", specifier = "==6.1.0" }, { name = "psycopg", specifier = "==3.3.2" }, { name = "psycopg-binary", specifier = "==3.3.2" }, @@ -370,13 +279,11 @@ requires-dist = [ { name = "rich", specifier = "==13.9.4" }, { name = "sqlalchemy", specifier = "==2.0.45" }, { name = "sqlglot", extras = ["rs"], specifier = "==27.8.0" }, - { name = "structlog", specifier = "==25.5.0" }, { name = "tinycss2", specifier = "==1.5.1" }, - { name = "tomli", marker = "python_full_version < '3.11'", specifier = ">=2.0.1" }, - { name = "tomli", marker = "python_full_version < '3.15'", specifier = ">=2.0.1" }, { name = "typer", specifier = "==0.19.0" }, { name = "user-agents", specifier = "==2.2.0" }, { name = "uvicorn", specifier = "==0.30.6" }, + { name = "weasyprint", specifier = "==63.1" }, { name = "websockets", specifier = ">=15.0.1,<16.0" }, ] @@ -388,15 +295,6 @@ dev = [ { name = "ruff", specifier = "==0.14.13" }, ] -[[package]] -name = "backports-asyncio-runner" -version = "1.2.0" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/8e/ff/70dca7d7cb1cbc0edb2c6cc0c38b65cba36cccc491eca64cabd5fe7f8670/backports_asyncio_runner-1.2.0.tar.gz", hash = "sha256:a5aa7b2b7d8f8bfcaa2b57313f70792df84e32a2a746f585213373f900b42162", size = 69893, upload-time = "2025-07-02T02:27:15.685Z" } -wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a0/59/76ab57e3fe74484f48a53f8e337171b4a2349e506eabe136d7e01d059086/backports_asyncio_runner-1.2.0-py3-none-any.whl", hash = "sha256:0da0a936a8aeb554eccb426dc55af3ba63bcdc69fa1a600b5bb305413a4477b5", size = 12313, upload-time = "2025-07-02T02:27:14.263Z" }, -] - [[package]] name = "bcrypt" version = "4.0.1" @@ -416,15 +314,6 @@ wheels = [ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/46/81/d8c22cd7e5e1c6a7d48e41a1d1d46c92f17dae70a54d9814f746e6027dec/bcrypt-4.0.1-cp36-abi3-win_amd64.whl", hash = "sha256:8a68f4341daf7522fe8d73874de8906f3a339048ba406be6ddc1b3ccb16fc0d9", size = 152930, upload-time = "2022-10-09T15:36:34.635Z" }, ] -[[package]] -name = "beartype" -version = "0.22.9" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c7/94/1009e248bbfbab11397abca7193bea6626806be9a327d399810d523a07cb/beartype-0.22.9.tar.gz", hash = "sha256:8f82b54aa723a2848a56008d18875f91c1db02c32ef6a62319a002e3e25a975f", size = 1608866, upload-time = "2025-12-13T06:50:30.72Z" } -wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/71/cc/18245721fa7747065ab478316c7fea7c74777d07f37ae60db2e84f8172e8/beartype-0.22.9-py3-none-any.whl", hash = "sha256:d16c9bbc61ea14637596c5f6fbff2ee99cbe3573e46a716401734ef50c3060c2", size = 1333658, upload-time = "2025-12-13T06:50:28.266Z" }, -] - [[package]] name = "bleach" version = "6.3.0" @@ -447,12 +336,62 @@ wheels = [ ] [[package]] -name = "cachetools" -version = "7.0.5" +name = "brotli" +version = "1.2.0" source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/af/dd/57fe3fdb6e65b25a5987fd2cdc7e22db0aef508b91634d2e57d22928d41b/cachetools-7.0.5.tar.gz", hash = "sha256:0cd042c24377200c1dcd225f8b7b12b0ca53cc2c961b43757e774ebe190fd990", size = 37367, upload-time = "2026-03-09T20:51:29.451Z" } +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f7/16/c92ca344d646e71a43b8bb353f0a6490d7f6e06210f8554c8f874e454285/brotli-1.2.0.tar.gz", hash = "sha256:e310f77e41941c13340a95976fe66a8a95b01e783d430eeaf7a2f87e0a57dd0a", size = 7388632, upload-time = "2025-11-05T18:39:42.86Z" } wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/06/f3/39cf3367b8107baa44f861dc802cbf16263c945b62d8265d36034fc07bea/cachetools-7.0.5-py3-none-any.whl", hash = "sha256:46bc8ebefbe485407621d0a4264b23c080cedd913921bad7ac3ed2f26c183114", size = 13918, upload-time = "2026-03-09T20:51:27.33Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/11/ee/b0a11ab2315c69bb9b45a2aaed022499c9c24a205c3a49c3513b541a7967/brotli-1.2.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:35d382625778834a7f3061b15423919aa03e4f5da34ac8e02c074e4b75ab4f84", size = 861543, upload-time = "2025-11-05T18:38:24.183Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e1/2f/29c1459513cd35828e25531ebfcbf3e92a5e49f560b1777a9af7203eb46e/brotli-1.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7a61c06b334bd99bc5ae84f1eeb36bfe01400264b3c352f968c6e30a10f9d08b", size = 444288, upload-time = "2025-11-05T18:38:25.139Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/3d/6f/feba03130d5fceadfa3a1bb102cb14650798c848b1df2a808356f939bb16/brotli-1.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:acec55bb7c90f1dfc476126f9711a8e81c9af7fb617409a9ee2953115343f08d", size = 1528071, upload-time = "2025-11-05T18:38:26.081Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/2b/38/f3abb554eee089bd15471057ba85f47e53a44a462cfce265d9bf7088eb09/brotli-1.2.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:260d3692396e1895c5034f204f0db022c056f9e2ac841593a4cf9426e2a3faca", size = 1626913, upload-time = "2025-11-05T18:38:27.284Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/03/a7/03aa61fbc3c5cbf99b44d158665f9b0dd3d8059be16c460208d9e385c837/brotli-1.2.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:072e7624b1fc4d601036ab3f4f27942ef772887e876beff0301d261210bca97f", size = 1419762, upload-time = "2025-11-05T18:38:28.295Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/21/1b/0374a89ee27d152a5069c356c96b93afd1b94eae83f1e004b57eb6ce2f10/brotli-1.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:adedc4a67e15327dfdd04884873c6d5a01d3e3b6f61406f99b1ed4865a2f6d28", size = 1484494, upload-time = "2025-11-05T18:38:29.29Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/cf/57/69d4fe84a67aef4f524dcd075c6eee868d7850e85bf01d778a857d8dbe0a/brotli-1.2.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7a47ce5c2288702e09dc22a44d0ee6152f2c7eda97b3c8482d826a1f3cfc7da7", size = 1593302, upload-time = "2025-11-05T18:38:30.639Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d5/3b/39e13ce78a8e9a621c5df3aeb5fd181fcc8caba8c48a194cd629771f6828/brotli-1.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:af43b8711a8264bb4e7d6d9a6d004c3a2019c04c01127a868709ec29962b6036", size = 1487913, upload-time = "2025-11-05T18:38:31.618Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/62/28/4d00cb9bd76a6357a66fcd54b4b6d70288385584063f4b07884c1e7286ac/brotli-1.2.0-cp312-cp312-win32.whl", hash = "sha256:e99befa0b48f3cd293dafeacdd0d191804d105d279e0b387a32054c1180f3161", size = 334362, upload-time = "2025-11-05T18:38:32.939Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/1c/4e/bc1dcac9498859d5e353c9b153627a3752868a9d5f05ce8dedd81a2354ab/brotli-1.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:b35c13ce241abdd44cb8ca70683f20c0c079728a36a996297adb5334adfc1c44", size = 369115, upload-time = "2025-11-05T18:38:33.765Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/6c/d4/4ad5432ac98c73096159d9ce7ffeb82d151c2ac84adcc6168e476bb54674/brotli-1.2.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9e5825ba2c9998375530504578fd4d5d1059d09621a02065d1b6bfc41a8e05ab", size = 861523, upload-time = "2025-11-05T18:38:34.67Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/91/9f/9cc5bd03ee68a85dc4bc89114f7067c056a3c14b3d95f171918c088bf88d/brotli-1.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0cf8c3b8ba93d496b2fae778039e2f5ecc7cff99df84df337ca31d8f2252896c", size = 444289, upload-time = "2025-11-05T18:38:35.6Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/2e/b6/fe84227c56a865d16a6614e2c4722864b380cb14b13f3e6bef441e73a85a/brotli-1.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c8565e3cdc1808b1a34714b553b262c5de5fbda202285782173ec137fd13709f", size = 1528076, upload-time = "2025-11-05T18:38:36.639Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/55/de/de4ae0aaca06c790371cf6e7ee93a024f6b4bb0568727da8c3de112e726c/brotli-1.2.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:26e8d3ecb0ee458a9804f47f21b74845cc823fd1bb19f02272be70774f56e2a6", size = 1626880, upload-time = "2025-11-05T18:38:37.623Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/5f/16/a1b22cbea436642e071adcaf8d4b350a2ad02f5e0ad0da879a1be16188a0/brotli-1.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:67a91c5187e1eec76a61625c77a6c8c785650f5b576ca732bd33ef58b0dff49c", size = 1419737, upload-time = "2025-11-05T18:38:38.729Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/46/63/c968a97cbb3bdbf7f974ef5a6ab467a2879b82afbc5ffb65b8acbb744f95/brotli-1.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4ecdb3b6dc36e6d6e14d3a1bdc6c1057c8cbf80db04031d566eb6080ce283a48", size = 1484440, upload-time = "2025-11-05T18:38:39.916Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/06/9d/102c67ea5c9fc171f423e8399e585dabea29b5bc79b05572891e70013cdd/brotli-1.2.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3e1b35d56856f3ed326b140d3c6d9db91740f22e14b06e840fe4bb1923439a18", size = 1593313, upload-time = "2025-11-05T18:38:41.24Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/9e/4a/9526d14fa6b87bc827ba1755a8440e214ff90de03095cacd78a64abe2b7d/brotli-1.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:54a50a9dad16b32136b2241ddea9e4df159b41247b2ce6aac0b3276a66a8f1e5", size = 1487945, upload-time = "2025-11-05T18:38:42.277Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/5b/e8/3fe1ffed70cbef83c5236166acaed7bb9c766509b157854c80e2f766b38c/brotli-1.2.0-cp313-cp313-win32.whl", hash = "sha256:1b1d6a4efedd53671c793be6dd760fcf2107da3a52331ad9ea429edf0902f27a", size = 334368, upload-time = "2025-11-05T18:38:43.345Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ff/91/e739587be970a113b37b821eae8097aac5a48e5f0eca438c22e4c7dd8648/brotli-1.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:b63daa43d82f0cdabf98dee215b375b4058cce72871fd07934f179885aad16e8", size = 369116, upload-time = "2025-11-05T18:38:44.609Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/17/e1/298c2ddf786bb7347a1cd71d63a347a79e5712a7c0cba9e3c3458ebd976f/brotli-1.2.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:6c12dad5cd04530323e723787ff762bac749a7b256a5bece32b2243dd5c27b21", size = 863080, upload-time = "2025-11-05T18:38:45.503Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/84/0c/aac98e286ba66868b2b3b50338ffbd85a35c7122e9531a73a37a29763d38/brotli-1.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:3219bd9e69868e57183316ee19c84e03e8f8b5a1d1f2667e1aa8c2f91cb061ac", size = 445453, upload-time = "2025-11-05T18:38:46.433Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ec/f1/0ca1f3f99ae300372635ab3fe2f7a79fa335fee3d874fa7f9e68575e0e62/brotli-1.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:963a08f3bebd8b75ac57661045402da15991468a621f014be54e50f53a58d19e", size = 1528168, upload-time = "2025-11-05T18:38:47.371Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d6/a6/2ebfc8f766d46df8d3e65b880a2e220732395e6d7dc312c1e1244b0f074a/brotli-1.2.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9322b9f8656782414b37e6af884146869d46ab85158201d82bab9abbcb971dc7", size = 1627098, upload-time = "2025-11-05T18:38:48.385Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f3/2f/0976d5b097ff8a22163b10617f76b2557f15f0f39d6a0fe1f02b1a53e92b/brotli-1.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cf9cba6f5b78a2071ec6fb1e7bd39acf35071d90a81231d67e92d637776a6a63", size = 1419861, upload-time = "2025-11-05T18:38:49.372Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/9c/97/d76df7176a2ce7616ff94c1fb72d307c9a30d2189fe877f3dd99af00ea5a/brotli-1.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7547369c4392b47d30a3467fe8c3330b4f2e0f7730e45e3103d7d636678a808b", size = 1484594, upload-time = "2025-11-05T18:38:50.655Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d3/93/14cf0b1216f43df5609f5b272050b0abd219e0b54ea80b47cef9867b45e7/brotli-1.2.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:fc1530af5c3c275b8524f2e24841cbe2599d74462455e9bae5109e9ff42e9361", size = 1593455, upload-time = "2025-11-05T18:38:51.624Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b3/73/3183c9e41ca755713bdf2cc1d0810df742c09484e2e1ddd693bee53877c1/brotli-1.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d2d085ded05278d1c7f65560aae97b3160aeb2ea2c0b3e26204856beccb60888", size = 1488164, upload-time = "2025-11-05T18:38:53.079Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/64/6a/0c78d8f3a582859236482fd9fa86a65a60328a00983006bcf6d83b7b2253/brotli-1.2.0-cp314-cp314-win32.whl", hash = "sha256:832c115a020e463c2f67664560449a7bea26b0c1fdd690352addad6d0a08714d", size = 339280, upload-time = "2025-11-05T18:38:54.02Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f5/10/56978295c14794b2c12007b07f3e41ba26acda9257457d7085b0bb3bb90c/brotli-1.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:e7c0af964e0b4e3412a0ebf341ea26ec767fa0b4cf81abb5e897c9338b5ad6a3", size = 375639, upload-time = "2025-11-05T18:38:55.67Z" }, +] + +[[package]] +name = "brotlicffi" +version = "1.2.0.1" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +dependencies = [ + { name = "cffi" }, +] +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/8a/b6/017dc5f852ed9b8735af77774509271acbf1de02d238377667145fcee01d/brotlicffi-1.2.0.1.tar.gz", hash = "sha256:c20d5c596278307ad06414a6d95a892377ea274a5c6b790c2548c009385d621c", size = 478156, upload-time = "2026-03-05T19:54:11.547Z" } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ef/f9/dfa56316837fa798eac19358351e974de8e1e2ca9475af4cb90293cd6576/brotlicffi-1.2.0.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2c85e65913cf2b79c57a3fdd05b98d9731d9255dc0cb696b09376cc091b9cddd", size = 433046, upload-time = "2026-03-05T19:53:46.209Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/4a/f5/f8f492158c76b0d940388801f04f747028971ad5774287bded5f1e53f08d/brotlicffi-1.2.0.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:535f2d05d0273408abc13fc0eebb467afac17b0ad85090c8913690d40207dac5", size = 1541126, upload-time = "2026-03-05T19:53:48.248Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/3b/e1/ff87af10ac419600c63e9287a0649c673673ae6b4f2bcf48e96cb2f89f60/brotlicffi-1.2.0.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ce17eb798ca59ecec67a9bb3fd7a4304e120d1cd02953ce522d959b9a84d58ac", size = 1541983, upload-time = "2026-03-05T19:53:50.317Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/47/c0/80ecd9bd45776109fab14040e478bf63e456967c9ddee2353d8330ed8de1/brotlicffi-1.2.0.1-cp314-cp314t-win32.whl", hash = "sha256:3c9544f83cb715d95d7eab3af4adbbef8b2093ad6382288a83b3a25feb1a57ec", size = 349047, upload-time = "2026-03-05T19:53:52.215Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ab/98/13e5b250236a281b6cd9e92a01ee1ae231029fa78faee932ef3766e1cb24/brotlicffi-1.2.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:625f8115d32ae9c0740d01ea51518437c3fbaa3e78d41cb18459f6f7ac326000", size = 385652, upload-time = "2026-03-05T19:53:53.892Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/9a/9f/b98dcd4af47994cee97aebac866996a006a2e5fc1fd1e2b82a8ad95cf09c/brotlicffi-1.2.0.1-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:91ba5f0ccc040f6ff8f7efaf839f797723d03ed46acb8ae9408f99ffd2572cf4", size = 432608, upload-time = "2026-03-05T19:53:56.736Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b1/7a/ac4ee56595a061e3718a6d1ea7e921f4df156894acffb28ed88a1fd52022/brotlicffi-1.2.0.1-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be9a670c6811af30a4bd42d7116dc5895d3b41beaa8ed8a89050447a0181f5ce", size = 1534257, upload-time = "2026-03-05T19:53:58.667Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/99/39/e7410db7f6f56de57744ea52a115084ceb2735f4d44973f349bb92136586/brotlicffi-1.2.0.1-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f3314a3476f59e5443f9f72a6dff16edc0c3463c9b318feaef04ae3e4683f5a", size = 1536838, upload-time = "2026-03-05T19:54:00.705Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a6/75/6e7977d1935fc3fbb201cbd619be8f2c7aea25d40a096967132854b34708/brotlicffi-1.2.0.1-cp38-abi3-win32.whl", hash = "sha256:82ea52e2b5d3145b6c406ebd3efb0d55db718b7ad996bd70c62cec0439de1187", size = 343337, upload-time = "2026-03-05T19:54:02.446Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d8/ef/e7e485ce5e4ba3843a0a92feb767c7b6098fd6e65ce752918074d175ae71/brotlicffi-1.2.0.1-cp38-abi3-win_amd64.whl", hash = "sha256:da2e82a08e7778b8bc539d27ca03cdd684113e81394bfaaad8d0dfc6a17ddede", size = 379026, upload-time = "2026-03-05T19:54:04.322Z" }, ] [[package]] @@ -473,31 +412,6 @@ dependencies = [ ] sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/93/d7/516d984057745a6cd96575eea814fe1edd6646ee6efd552fb7b0921dec83/cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44", size = 184283, upload-time = "2025-09-08T23:22:08.01Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/9e/84/ad6a0b408daa859246f57c03efd28e5dd1b33c21737c2db84cae8c237aa5/cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49", size = 180504, upload-time = "2025-09-08T23:22:10.637Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/50/bd/b1a6362b80628111e6653c961f987faa55262b4002fcec42308cad1db680/cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c", size = 208811, upload-time = "2025-09-08T23:22:12.267Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/4f/27/6933a8b2562d7bd1fb595074cf99cc81fc3789f6a6c05cdabb46284a3188/cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb", size = 216402, upload-time = "2025-09-08T23:22:13.455Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/05/eb/b86f2a2645b62adcfff53b0dd97e8dfafb5c8aa864bd0d9a2c2049a0d551/cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0", size = 203217, upload-time = "2025-09-08T23:22:14.596Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/9f/e0/6cbe77a53acf5acc7c08cc186c9928864bd7c005f9efd0d126884858a5fe/cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4", size = 203079, upload-time = "2025-09-08T23:22:15.769Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/98/29/9b366e70e243eb3d14a5cb488dfd3a0b6b2f1fb001a203f653b93ccfac88/cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453", size = 216475, upload-time = "2025-09-08T23:22:17.427Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/21/7a/13b24e70d2f90a322f2900c5d8e1f14fa7e2a6b3332b7309ba7b2ba51a5a/cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495", size = 218829, upload-time = "2025-09-08T23:22:19.069Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/60/99/c9dc110974c59cc981b1f5b66e1d8af8af764e00f0293266824d9c4254bc/cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5", size = 211211, upload-time = "2025-09-08T23:22:20.588Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/49/72/ff2d12dbf21aca1b32a40ed792ee6b40f6dc3a9cf1644bd7ef6e95e0ac5e/cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb", size = 218036, upload-time = "2025-09-08T23:22:22.143Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e2/cc/027d7fb82e58c48ea717149b03bcadcbdc293553edb283af792bd4bcbb3f/cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a", size = 172184, upload-time = "2025-09-08T23:22:23.328Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/33/fa/072dd15ae27fbb4e06b437eb6e944e75b068deb09e2a2826039e49ee2045/cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739", size = 182790, upload-time = "2025-09-08T23:22:24.752Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, @@ -546,95 +460,6 @@ wheels = [ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, ] -[[package]] -name = "charset-normalizer" -version = "3.4.4" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } -wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/1f/b8/6d51fc1d52cbd52cd4ccedd5b5b2f0f6a11bbf6765c782298b0f3e808541/charset_normalizer-3.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d", size = 209709, upload-time = "2025-10-14T04:40:11.385Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/5c/af/1f9d7f7faafe2ddfb6f72a2e07a548a629c61ad510fe60f9630309908fef/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8", size = 148814, upload-time = "2025-10-14T04:40:13.135Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/79/3d/f2e3ac2bbc056ca0c204298ea4e3d9db9b4afe437812638759db2c976b5f/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad", size = 144467, upload-time = "2025-10-14T04:40:14.728Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ec/85/1bf997003815e60d57de7bd972c57dc6950446a3e4ccac43bc3070721856/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8", size = 162280, upload-time = "2025-10-14T04:40:16.14Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/3e/8e/6aa1952f56b192f54921c436b87f2aaf7c7a7c3d0d1a765547d64fd83c13/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d", size = 159454, upload-time = "2025-10-14T04:40:17.567Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/36/3b/60cbd1f8e93aa25d1c669c649b7a655b0b5fb4c571858910ea9332678558/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313", size = 153609, upload-time = "2025-10-14T04:40:19.08Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/64/91/6a13396948b8fd3c4b4fd5bc74d045f5637d78c9675585e8e9fbe5636554/charset_normalizer-3.4.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e", size = 151849, upload-time = "2025-10-14T04:40:20.607Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b7/7a/59482e28b9981d105691e968c544cc0df3b7d6133152fb3dcdc8f135da7a/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93", size = 151586, upload-time = "2025-10-14T04:40:21.719Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/92/59/f64ef6a1c4bdd2baf892b04cd78792ed8684fbc48d4c2afe467d96b4df57/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0", size = 145290, upload-time = "2025-10-14T04:40:23.069Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/6b/63/3bf9f279ddfa641ffa1962b0db6a57a9c294361cc2f5fcac997049a00e9c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84", size = 163663, upload-time = "2025-10-14T04:40:24.17Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ed/09/c9e38fc8fa9e0849b172b581fd9803bdf6e694041127933934184e19f8c3/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e", size = 151964, upload-time = "2025-10-14T04:40:25.368Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d2/d1/d28b747e512d0da79d8b6a1ac18b7ab2ecfd81b2944c4c710e166d8dd09c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db", size = 161064, upload-time = "2025-10-14T04:40:26.806Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/bb/9a/31d62b611d901c3b9e5500c36aab0ff5eb442043fb3a1c254200d3d397d9/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6", size = 155015, upload-time = "2025-10-14T04:40:28.284Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/1f/f3/107e008fa2bff0c8b9319584174418e5e5285fef32f79d8ee6a430d0039c/charset_normalizer-3.4.4-cp310-cp310-win32.whl", hash = "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f", size = 99792, upload-time = "2025-10-14T04:40:29.613Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/eb/66/e396e8a408843337d7315bab30dbf106c38966f1819f123257f5520f8a96/charset_normalizer-3.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d", size = 107198, upload-time = "2025-10-14T04:40:30.644Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b5/58/01b4f815bf0312704c267f2ccb6e5d42bcc7752340cd487bc9f8c3710597/charset_normalizer-3.4.4-cp310-cp310-win_arm64.whl", hash = "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69", size = 100262, upload-time = "2025-10-14T04:40:32.108Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", size = 206988, upload-time = "2025-10-14T04:40:33.79Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/94/59/2e87300fe67ab820b5428580a53cad894272dbb97f38a7a814a2a1ac1011/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", size = 147324, upload-time = "2025-10-14T04:40:34.961Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/07/fb/0cf61dc84b2b088391830f6274cb57c82e4da8bbc2efeac8c025edb88772/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", size = 142742, upload-time = "2025-10-14T04:40:36.105Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/62/8b/171935adf2312cd745d290ed93cf16cf0dfe320863ab7cbeeae1dcd6535f/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", size = 160863, upload-time = "2025-10-14T04:40:37.188Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/09/73/ad875b192bda14f2173bfc1bc9a55e009808484a4b256748d931b6948442/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", size = 157837, upload-time = "2025-10-14T04:40:38.435Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/6d/fc/de9cce525b2c5b94b47c70a4b4fb19f871b24995c728e957ee68ab1671ea/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", size = 151550, upload-time = "2025-10-14T04:40:40.053Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/55/c2/43edd615fdfba8c6f2dfbd459b25a6b3b551f24ea21981e23fb768503ce1/charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", size = 149162, upload-time = "2025-10-14T04:40:41.163Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/03/86/bde4ad8b4d0e9429a4e82c1e8f5c659993a9a863ad62c7df05cf7b678d75/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", size = 150019, upload-time = "2025-10-14T04:40:42.276Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/1f/86/a151eb2af293a7e7bac3a739b81072585ce36ccfb4493039f49f1d3cae8c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", size = 143310, upload-time = "2025-10-14T04:40:43.439Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b5/fe/43dae6144a7e07b87478fdfc4dbe9efd5defb0e7ec29f5f58a55aeef7bf7/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", size = 162022, upload-time = "2025-10-14T04:40:44.547Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/80/e6/7aab83774f5d2bca81f42ac58d04caf44f0cc2b65fc6db2b3b2e8a05f3b3/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", size = 149383, upload-time = "2025-10-14T04:40:46.018Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/4f/e8/b289173b4edae05c0dde07f69f8db476a0b511eac556dfe0d6bda3c43384/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", size = 159098, upload-time = "2025-10-14T04:40:47.081Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d8/df/fe699727754cae3f8478493c7f45f777b17c3ef0600e28abfec8619eb49c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", size = 152991, upload-time = "2025-10-14T04:40:48.246Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/1a/86/584869fe4ddb6ffa3bd9f491b87a01568797fb9bd8933f557dba9771beaf/charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", size = 99456, upload-time = "2025-10-14T04:40:49.376Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/65/f6/62fdd5feb60530f50f7e38b4f6a1d5203f4d16ff4f9f0952962c044e919a/charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", size = 106978, upload-time = "2025-10-14T04:40:50.844Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/7a/9d/0710916e6c82948b3be62d9d398cb4fcf4e97b56d6a6aeccd66c4b2f2bd5/charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", size = 99969, upload-time = "2025-10-14T04:40:52.272Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497, upload-time = "2025-10-14T04:40:57.217Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240, upload-time = "2025-10-14T04:40:58.358Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471, upload-time = "2025-10-14T04:40:59.468Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864, upload-time = "2025-10-14T04:41:00.623Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647, upload-time = "2025-10-14T04:41:01.754Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110, upload-time = "2025-10-14T04:41:03.231Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839, upload-time = "2025-10-14T04:41:04.715Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667, upload-time = "2025-10-14T04:41:05.827Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535, upload-time = "2025-10-14T04:41:06.938Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816, upload-time = "2025-10-14T04:41:08.101Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, -] - [[package]] name = "click" version = "8.1.7" @@ -647,15 +472,6 @@ wheels = [ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/00/2e/d53fa4befbf2cfa713304affc7ca780ce4fc1fd8710527771b58311a3229/click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", size = 97941, upload-time = "2023-08-17T17:29:10.08Z" }, ] -[[package]] -name = "cloudpickle" -version = "3.1.2" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/27/fb/576f067976d320f5f0114a8d9fa1215425441bb35627b1993e5afd8111e5/cloudpickle-3.1.2.tar.gz", hash = "sha256:7fda9eb655c9c230dab534f1983763de5835249750e85fbcef43aaa30a9a2414", size = 22330, upload-time = "2025-11-03T09:25:26.604Z" } -wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl", hash = "sha256:9acb47f6afd73f60dc1df93bb801b472f05ff42fa6c84167d25cb206be1fbf4a", size = 22228, upload-time = "2025-11-03T09:25:25.534Z" }, -] - [[package]] name = "colorama" version = "0.4.6" @@ -665,15 +481,6 @@ wheels = [ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] -[[package]] -name = "coolname" -version = "4.1.0" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/50/97/c6d3911b2c9ba7c129820bbbd2ef1e9bae8402aa70f15820dc3a9d770e87/coolname-4.1.0.tar.gz", hash = "sha256:1db8b0857d0e40795cf555f449542cc3b8fe4a85b64ae3507fb475bafa2924fa", size = 38433, upload-time = "2026-03-16T18:52:31.875Z" } -wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f9/96/e0ea059650e0db3755d2e734cd65fb15aa5546e5cdf96e7e68ae018cc994/coolname-4.1.0-py3-none-any.whl", hash = "sha256:2721ab2112788ad3f92b398ed48533d58be30450c4e7b266f9407fcdf37c8d24", size = 39907, upload-time = "2026-03-16T18:52:30.303Z" }, -] - [[package]] name = "croniter" version = "4.0.0" @@ -687,14 +494,6 @@ wheels = [ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/4e/a5/b4cbef8a130382a84fc9342c2d1b2551dcf2817e768bb3e2881b71617119/croniter-4.0.0-py2.py3-none-any.whl", hash = "sha256:312717b8017ad6052817934af563f6754b1f25cb942dcca1e617240aca4aa254", size = 23411, upload-time = "2024-10-28T11:07:55.024Z" }, ] -[[package]] -name = "cronsim" -version = "2.7" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } -wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/fb/1a/02f105147f7f2e06ed4f734ff5a6439590bb275a53dd91fc73df6312298a/cronsim-2.7-py3-none-any.whl", hash = "sha256:1e1431fa08c51dc7f72e67e571c7c7a09af26420169b607badd4ca9677ffad1e", size = 14213, upload-time = "2025-10-21T16:38:20.431Z" }, -] - [[package]] name = "cryptography" version = "45.0.2" @@ -728,50 +527,19 @@ wheels = [ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b9/d4/75d2375a20d80aa262a8adee77bf56950e9292929e394b9fae2481803f11/cryptography-45.0.2-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:c0c000c1a09f069632d8a9eb3b610ac029fcc682f1d69b758e625d6ee713f4ed", size = 4560535, upload-time = "2025-05-18T02:45:59.33Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/aa/18/c3a94474987ebcfb88692036b2ec44880d243fefa73794bdcbf748679a6e/cryptography-45.0.2-cp37-abi3-win32.whl", hash = "sha256:08281de408e7eb71ba3cd5098709a356bfdf65eebd7ee7633c3610f0aa80d79b", size = 2922045, upload-time = "2025-05-18T02:46:01.012Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/63/63/fb28b30c144182fd44ce93d13ab859791adbf923e43bdfb610024bfecda1/cryptography-45.0.2-cp37-abi3-win_amd64.whl", hash = "sha256:48caa55c528617fa6db1a9c3bf2e37ccb31b73e098ac2b71408d1f2db551dde4", size = 3393321, upload-time = "2025-05-18T02:46:03.441Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/6e/f5/d1d4dead3b269671cda7be6d6b2970b25398e84219681cb397139bdce88b/cryptography-45.0.2-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a8ec324711596fbf21837d3a5db543937dd84597d364769b46e0102250023f77", size = 3578517, upload-time = "2025-05-18T02:46:05.263Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ac/7b/00e18d24f08bc642e4018e0066a6f872d85c744e3265910c3beabb1f4d73/cryptography-45.0.2-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:965611880c3fa8e504b7458484c0697e00ae6e937279cd6734fdaa2bc954dc49", size = 4135515, upload-time = "2025-05-18T02:46:07.241Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/29/9f/ea7ad5239c33c36f0e2cbdf631a0e3b7633466e87e55923f5b5ea1b0b92d/cryptography-45.0.2-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:d891942592789fa0ab71b502550bbadb12f540d7413d7d7c4cef4b02af0f5bc6", size = 4378133, upload-time = "2025-05-18T02:46:09.035Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/47/f8/b4e29d87fbc4d2cf46b36e01fcb98305bf76699f34de6b877cddd8bc3a64/cryptography-45.0.2-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:b19f4b28dd2ef2e6d600307fee656c00825a2980c4356a7080bd758d633c3a6f", size = 4136787, upload-time = "2025-05-18T02:46:10.772Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/dc/7c/ac19bbf24d261667a67aac712d8aa3bb740f94bc2391f06ccc90e783f3ff/cryptography-45.0.2-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:7c73968fbb7698a4c5d6160859db560d3aac160edde89c751edd5a8bc6560c88", size = 4377741, upload-time = "2025-05-18T02:46:13.215Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e2/69/51f1c3d03ef4e3bcac4d3f00738f6ac0a205199809e2b92368b6f15a9ec4/cryptography-45.0.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:501de1296b2041dccf2115e3c7d4947430585601b251b140970ce255c5cfb985", size = 3326934, upload-time = "2025-05-18T02:46:15.081Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d7/74/2a0fb642c4c34d8c46c12b6eac89b10769b378c7b6a901ff94a8d4ba1b52/cryptography-45.0.2-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:1655d3a76e3dedb683c982a6c3a2cbfae2d08f47a48ec5a3d58db52b3d29ea6f", size = 3587805, upload-time = "2025-05-18T02:46:17.531Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/8a/18/57bc98fa5d93e74c2c2b16a3c5383f7ec218f957aa44559c0008a46c3629/cryptography-45.0.2-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:dc7693573f16535428183de8fd27f0ca1ca37a51baa0b41dc5ed7b3d68fe80e2", size = 4143347, upload-time = "2025-05-18T02:46:19.934Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/84/6f/d015e7e7bd7f3a6c538973005de5a780d93b68138c2d88c804422cf46b1c/cryptography-45.0.2-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:614bca7c6ed0d8ad1dce683a6289afae1f880675b4090878a0136c3da16bc693", size = 4387414, upload-time = "2025-05-18T02:46:21.944Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/de/9e/fa5ec89cce7e4b86e430438da4d66b79113bdf321d0a00167d34b61daf19/cryptography-45.0.2-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:4142e20c29224cec63e9e32eb1e6014fb285fe39b7be66b3564ca978a3a8afe9", size = 4145849, upload-time = "2025-05-18T02:46:24.327Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/7c/09/5887d4fcc6f9c6fb19920789d094c4e25c2f604cc1b10b7e69d6f56187fe/cryptography-45.0.2-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:9a900036b42f7324df7c7ad9569eb92ba0b613cf699160dd9c2154b24fd02f8e", size = 4387449, upload-time = "2025-05-18T02:46:26.144Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a5/4a/e27ab71dc3e517becc3f2ae358454bb4b78c0cb5af52f8e11b8943525ea6/cryptography-45.0.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:057723b79752a142efbc609e90b0dff27b0361ccbee3bd48312d70f5cdf53b78", size = 3335090, upload-time = "2025-05-18T02:46:27.913Z" }, ] [[package]] -name = "cyclopts" -version = "4.10.1" +name = "cssselect2" +version = "0.9.0" source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } dependencies = [ - { name = "attrs" }, - { name = "docstring-parser" }, - { name = "rich" }, - { name = "rich-rst" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, + { name = "tinycss2" }, + { name = "webencodings" }, ] -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/6c/c4/2ce2ca1451487dc7d59f09334c3fa1182c46cfcf0a2d5f19f9b26d53ac74/cyclopts-4.10.1.tar.gz", hash = "sha256:ad4e4bb90576412d32276b14a76f55d43353753d16217f2c3cd5bdceba7f15a0", size = 166623, upload-time = "2026-03-23T14:43:01.098Z" } +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e0/20/92eaa6b0aec7189fa4b75c890640e076e9e793095721db69c5c81142c2e1/cssselect2-0.9.0.tar.gz", hash = "sha256:759aa22c216326356f65e62e791d66160a0f9c91d1424e8d8adc5e74dddfc6fb", size = 35595, upload-time = "2026-02-12T17:16:39.614Z" } wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/8a/0b/2261922126b2e50c601fe22d7ff5194e0a4d50e654836260c0665e24d862/cyclopts-4.10.1-py3-none-any.whl", hash = "sha256:35f37257139380a386d9fe4475e1e7c87ca7795765ef4f31abba579fcfcb6ecd", size = 204331, upload-time = "2026-03-23T14:43:02.625Z" }, -] - -[[package]] -name = "dateparser" -version = "1.4.0" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } -dependencies = [ - { name = "python-dateutil" }, - { name = "pytz" }, - { name = "regex" }, - { name = "tzlocal" }, -] -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/46/2d/a0ccdb78788064fa0dc901b8524e50615c42be1d78b78d646d0b28d09180/dateparser-1.4.0.tar.gz", hash = "sha256:97a21840d5ecdf7630c584f673338a5afac5dfe84f647baf4d7e8df98f9354a4", size = 321512, upload-time = "2026-03-26T09:56:10.292Z" } -wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b4/0b/3c3bb7cbe757279e693a0be6049048012f794d01f81099609ecd53b899f0/dateparser-1.4.0-py3-none-any.whl", hash = "sha256:7902b8e85d603494bf70a5a0b1decdddb2270b9c6e6b2bc8a57b93476c0df378", size = 300379, upload-time = "2026-03-26T09:56:08.409Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/21/0e/8459ca4413e1a21a06c97d134bfaf18adfd27cea068813dc0faae06cbf00/cssselect2-0.9.0-py3-none-any.whl", hash = "sha256:6a99e5f91f9a016a304dd929b0966ca464bcfda15177b6fb4a118fc0fb5d9563", size = 15453, upload-time = "2026-02-12T17:16:38.317Z" }, ] [[package]] @@ -792,20 +560,6 @@ wheels = [ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", size = 331094, upload-time = "2025-09-07T18:57:58.071Z" }, ] -[[package]] -name = "docker" -version = "7.1.0" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } -dependencies = [ - { name = "pywin32", marker = "sys_platform == 'win32'" }, - { name = "requests" }, - { name = "urllib3" }, -] -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/91/9b/4a2ea29aeba62471211598dac5d96825bb49348fa07e906ea930394a83ce/docker-7.1.0.tar.gz", hash = "sha256:ad8c70e6e3f8926cb8a92619b832b4ea5299e2831c14284663184e200546fa6c", size = 117834, upload-time = "2024-05-23T11:13:57.216Z" } -wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e3/26/57c6fb270950d476074c087527a558ccb6f4436657314bfb6cdf484114c4/docker-7.1.0-py3-none-any.whl", hash = "sha256:c96b93b7f0a746f9e77d325bcfb87422a3d8bd4f03136ae8a85b37f1898d5fc0", size = 147774, upload-time = "2024-05-23T11:13:55.01Z" }, -] - [[package]] name = "docstring-parser" version = "0.17.0" @@ -815,15 +569,6 @@ wheels = [ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/55/e2/2537ebcff11c1ee1ff17d8d0b6f4db75873e3b0fb32c2d4a2ee31ecb310a/docstring_parser-0.17.0-py3-none-any.whl", hash = "sha256:cf2569abd23dce8099b300f9b4fa8191e9582dda731fd533daf54c4551658708", size = 36896, upload-time = "2025-07-21T07:35:00.684Z" }, ] -[[package]] -name = "docutils" -version = "0.22.4" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ae/b6/03bb70946330e88ffec97aefd3ea75ba575cb2e762061e0e62a213befee8/docutils-0.22.4.tar.gz", hash = "sha256:4db53b1fde9abecbb74d91230d32ab626d94f6badfc575d6db9194a49df29968", size = 2291750, upload-time = "2025-12-18T19:00:26.443Z" } -wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/02/10/5da547df7a391dcde17f59520a231527b8571e6f46fc8efb02ccb370ab12/docutils-0.22.4-py3-none-any.whl", hash = "sha256:d0013f540772d1420576855455d050a2180186c91c15779301ac2ccb3eeb68de", size = 633196, upload-time = "2025-12-18T19:00:18.077Z" }, -] - [[package]] name = "email-validator" version = "2.3.0" @@ -846,18 +591,6 @@ wheels = [ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c1/8b/5fe2cc11fee489817272089c4203e679c63b570a5aaeb18d852ae3cbba6a/et_xmlfile-2.0.0-py3-none-any.whl", hash = "sha256:7a91720bc756843502c3b7504c77b8fe44217c85c537d85037f0f536151b2caa", size = 18059, upload-time = "2024-10-25T17:25:39.051Z" }, ] -[[package]] -name = "exceptiongroup" -version = "1.3.1" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } -dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, -] -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } -wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, -] - [[package]] name = "fakeredis" version = "2.34.1" @@ -865,18 +598,12 @@ source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } dependencies = [ { name = "redis" }, { name = "sortedcontainers" }, - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/11/40/fd09efa66205eb32253d2b2ebc63537281384d2040f0a88bcd2289e120e4/fakeredis-2.34.1.tar.gz", hash = "sha256:4ff55606982972eecce3ab410e03d746c11fe5deda6381d913641fbd8865ea9b", size = 177315, upload-time = "2026-02-25T13:17:51.315Z" } wheels = [ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/49/b5/82f89307d0d769cd9bf46a54fb9136be08e4e57c5570ae421db4c9a2ba62/fakeredis-2.34.1-py3-none-any.whl", hash = "sha256:0107ec99d48913e7eec2a5e3e2403d1bd5f8aa6489d1a634571b975289c48f12", size = 122160, upload-time = "2026-02-25T13:17:49.701Z" }, ] -[package.optional-dependencies] -lua = [ - { name = "lupa" }, -] - [[package]] name = "fastapi" version = "0.115.2" @@ -942,12 +669,51 @@ wheels = [ ] [[package]] -name = "fsspec" -version = "2026.2.0" +name = "fonttools" +version = "4.63.0" source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/51/7c/f60c259dcbf4f0c47cc4ddb8f7720d2dcdc8888c8e5ad84c73ea4531cc5b/fsspec-2026.2.0.tar.gz", hash = "sha256:6544e34b16869f5aacd5b90bdf1a71acb37792ea3ddf6125ee69a22a53fb8bff", size = 313441, upload-time = "2026-02-05T21:50:53.743Z" } +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/84/69/c97f2c18e0db87d2c7b15da1974dace76ae938f1cfa22e2727a648b7ed43/fonttools-4.63.0.tar.gz", hash = "sha256:caeb583deeb5168e694b65cda8b4ee62abedfa66cf88488734466f2366b9c4e0", size = 3597189, upload-time = "2026-05-14T12:04:30.958Z" } wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e6/ab/fb21f4c939bb440104cc2b396d3be1d9b7a9fd3c6c2a53d98c45b3d7c954/fsspec-2026.2.0-py3-none-any.whl", hash = "sha256:98de475b5cb3bd66bedd5c4679e87b4fdfe1a3bf4d707b151b3c07e58c9a2437", size = 202505, upload-time = "2026-02-05T21:50:51.819Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/08/ef/b3c6b9b5be2f82416d73fe2ed2e96e2793cd80e7510bd6a17ca79cdd88ec/fonttools-4.63.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:37dd23e621e3b0aef1baa70a303b80aaf38449632cfc8fd2a55fb285bbccfc02", size = 2881131, upload-time = "2026-05-14T12:03:13.386Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/44/a0/c815bea63117fa63e4e1c01f8a1110d2112fa003f838e6467094ec2432ce/fonttools-4.63.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a9faff9e0c1f76f9fd55899d2ce785832efebab37eb8ae13995853aef178bef0", size = 2426704, upload-time = "2026-05-14T12:03:15.801Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/44/04/0b91d8e916e92ad1fac9e4624760baf0fd5ff2ead614c2f68fb21373f03f/fonttools-4.63.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef3048ef05dbb552b89817713d9cac912e00d0fde4a3105c00d29e52e10c89af", size = 5044298, upload-time = "2026-05-14T12:03:18.085Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/77/c7/2342da9830e3e9d4870305ca5d2091d2a83284f2953079b7bdd3b5e029d8/fonttools-4.63.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:58dc6bb86a78d782f00f9190ca02c119cf5bbe2807536e361e18d42019f877d8", size = 4999800, upload-time = "2026-05-14T12:03:20.161Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e6/6d/67fe16c48d7ce050979b33f47e0d28a318f02da030602e944c34f7a16ef3/fonttools-4.63.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ee08ebfa58f6e1aeff5697ab9582105bb620008c1caafb681e4c557e7483027b", size = 4982666, upload-time = "2026-05-14T12:03:22.87Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f2/00/3bbab338c07c71fa56269953845e92c951a61457bbbb0f1022551ea266d9/fonttools-4.63.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:27fdc65af8da6f88b9c6121c47a464cbe359fcfff7ff6fc2d37a1f395d755b78", size = 5133598, upload-time = "2026-05-14T12:03:25.168Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/62/f2/aa27c7f98db5b064883dadcc5283947e81e034de42e22a33675878d98b54/fonttools-4.63.0-cp312-cp312-win32.whl", hash = "sha256:af2fd1664d00a397d75f806985ddb36282091c2131a73a6485c23b4a34722263", size = 2292575, upload-time = "2026-05-14T12:03:27.496Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/87/36/cccb9bc2a6ab63d1b2980374f0dca72ce95ae267c9b4cfe77455bb70d0d4/fonttools-4.63.0-cp312-cp312-win_amd64.whl", hash = "sha256:59ac449f8cca9b4ffa08d2e7bbadad87ce710d69d1eda5c3c1ce579baa987272", size = 2343211, upload-time = "2026-05-14T12:03:30.057Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/0f/8d/d8fec3dcde2963f8c908fb315e5ff2cd0ac34f82394bbbf73a2aa5145ce3/fonttools-4.63.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:cd7e9857e5e63738b9d9fd707bc1f59c8b09e5177726d23664db393c59bb08bd", size = 2876062, upload-time = "2026-05-14T12:03:32.554Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ef/71/d935dc54e4ff121bfdd11e08702db63a7e6f25af21d8a3d7b7212df53641/fonttools-4.63.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c2a2a42198b696a6f48fad91709afb55176e66a5e566131219dba372fb7f8c59", size = 2424594, upload-time = "2026-05-14T12:03:34.86Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/8e/40/e76320afa1df918e146155ef239b1719ee266092e96f5423bfd075affba1/fonttools-4.63.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e874792a8212b44583ea02189d9e693906b2f78b261f372f95d6c563210ac1d", size = 5024840, upload-time = "2026-05-14T12:03:36.745Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ce/36/0b805d8c485f872f65a509cbe3b58a5d0d17bee855333b54a150c79d3061/fonttools-4.63.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:22135da48a348785c5e2d5d2d9d6bec5ed44adacbaeb9db12d9493bf6c6bfa68", size = 4975801, upload-time = "2026-05-14T12:03:38.833Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c8/26/2cee03d0aa083ab022da5c07aff9ed3f689da1defb81ad6917c9627896da/fonttools-4.63.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ccf41f2efdf56994d22d73bef4ced1052161958169428d06ba9724ea9e9a64be", size = 4965009, upload-time = "2026-05-14T12:03:41.494Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/7e/48/cc4b66d9058c0d0982c833fad10127c4b0e9324606aafa41382295ca4102/fonttools-4.63.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9ced0bd02ac751dd6319b0da88aaef24414e3b0dbc32bb4f24944821a3741a27", size = 5105892, upload-time = "2026-05-14T12:03:43.525Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d8/1f/a98a30a814b9ddef3a2e706025f90b9e0bc94890e6cb15254bc86547d11a/fonttools-4.63.0-cp313-cp313-win32.whl", hash = "sha256:85be818f5506e8a7753153def2c9550178f0ecae6a47b5e0e8dbb23f7cc90380", size = 2291313, upload-time = "2026-05-14T12:03:45.594Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/92/46/5177b01f3b4abfdd4409f31cca4ab279c9343a26efbe9ec78c97fc612e02/fonttools-4.63.0-cp313-cp313-win_amd64.whl", hash = "sha256:ba04cb5891d4c0c21b6da95eda8d7b090021508a294fff33464fc7d241e0856b", size = 2342299, upload-time = "2026-05-14T12:03:47.414Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/27/d2/23d25e3f247b328be58d04a4c9f894178a0d1eda7d42867cfb388adaf416/fonttools-4.63.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fd1e3094f42d806d3d7c79162fc59e5910fcbe3a7360c385b8da969bc4493745", size = 2875338, upload-time = "2026-05-14T12:03:50.052Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/cd/58/7dfa0c761cb3b2964e2a84c4dc986c926a87de0cb9fb60d5b28ded3f2914/fonttools-4.63.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:6e528da43bc3791085f8cb6141b1d13e459226790240340fcbb4625649238b03", size = 2422661, upload-time = "2026-05-14T12:03:52.154Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/dd/87/64cfa18a7a1621d17b7f4502b2b0ed8a135a90c3db51ea590ee99043e76b/fonttools-4.63.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b2248c5decb223562f7902ff6325077a073f608ee8e33e88ad88db734eb9f49", size = 5010526, upload-time = "2026-05-14T12:03:54.647Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/36/e1/a8933a72c45a87177fbde2696e0d0755c8c9062f8c077a961c6215fa27b1/fonttools-4.63.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:308f957cdeaf8abe4e5f2f124902ef405448af92c90f80e302a3b771c2e6116b", size = 4923946, upload-time = "2026-05-14T12:03:56.984Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/27/60/872e6e233b8c5e8b41413796ff18b7fe479661bd40147e071b450dfad7a1/fonttools-4.63.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:bf00f21eb5fb721dbaf73d1e9da6d02a1af7768f2ebcf9798be98beab8ba90f6", size = 4962489, upload-time = "2026-05-14T12:03:59.443Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/30/c4/83c24f2ec38b90cfda84bf4b1a1f49df80e84a1db4e7ac6e0d41bf23bc39/fonttools-4.63.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c1aaa4b9c75798400ac043ce04d74e7830376c85095a5a6ed7cba2f17a266bf4", size = 5071870, upload-time = "2026-05-14T12:04:02.122Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/de/40/3ae22b60ff1d41ce0bd044b31238cdc72cef99f28b976f1e128ebd618c9b/fonttools-4.63.0-cp314-cp314-win32.whl", hash = "sha256:22693918177bd9ceabec4736d338045f357769416fc6b0b2508eefef75b08616", size = 2295026, upload-time = "2026-05-14T12:04:04.47Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c3/d4/98078064ccc76b45cb0f6c002452011e93c4bd26f6850344f0951cc1fe89/fonttools-4.63.0-cp314-cp314-win_amd64.whl", hash = "sha256:7d782fac32985914c351556f68ac0855391572bcd87de50e05970d3cd4c96fc5", size = 2347454, upload-time = "2026-05-14T12:04:06.752Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/49/4e/652d1580c5f4e39f7d103b0c793e4773129ad633dce4addd0cf4dfebde02/fonttools-4.63.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:6db5140a60a5d731d21ec076745b40a310607731b0a565b50776393188649001", size = 2958152, upload-time = "2026-05-14T12:04:08.706Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/0e/55/ad864c9a9b219f552eb46b32cd7906c466e5a578ba0c3abfcc0fe7413eb6/fonttools-4.63.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7d76edbff9014094dbf03bd2d074709dfa6ec7aba13d838c937a2b33d2d6a86e", size = 2460809, upload-time = "2026-05-14T12:04:10.783Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ea/2b/0aa8db70f18cf52e49b4ed5ecec68547f981160bf5ded3b5aed6faa0a6f9/fonttools-4.63.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0eac00b9118c3c2f87d272e45341871c5b3066baa3c86897fa634a7c3fb59096", size = 5148649, upload-time = "2026-05-14T12:04:12.747Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/7f/63/18e4369c25043096f1048e0c9915951adc4f842bd81c6b18155824d6fa99/fonttools-4.63.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:51394295f1a51de8b5f30bdb1e1b9a4231536c7064ef5c6e211eec19fa36036f", size = 4932147, upload-time = "2026-05-14T12:04:14.806Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a1/3f/67f3eac2ffd8a98446c5022f8ed3864eac878a5ff7af8df4c8286dba16cc/fonttools-4.63.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9e12f105d2b6342c559c298afb674006bb2893afc7102dcf8a1b55b0486b4e40", size = 5027237, upload-time = "2026-05-14T12:04:17.675Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/1a/ba/4e6214cb38a7b04779e97bb7636de9a5c7f20af7018d03dee0b64c08510a/fonttools-4.63.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:796f27556dbe094c4824f75ca85267e4df776c79036c8441469a4df37038c196", size = 5053933, upload-time = "2026-05-14T12:04:20.818Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/34/3b/214dcc19ee31d3d38fb5ad2755c11ef0514e5dc300bbaf41c0b69f393799/fonttools-4.63.0-cp314-cp314t-win32.whl", hash = "sha256:948428a275741f0b64b113c955425a953314f4b9ab9997f73a72c83e68e569c8", size = 2359326, upload-time = "2026-05-14T12:04:24.22Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/dd/1e/3ff1a9b523058c2eeb6a9d50f5574e2a738200d0d94107d5bc4105e8da3f/fonttools-4.63.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6d4741eb179121cab9eea4cb2393d24492373a260d7945006358c08cfbf45419", size = 2425829, upload-time = "2026-05-14T12:04:26.829Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/2c/47/c99d5268f354002ce80f8d029cd9d7d872969da1de8b93d32de4dc56d6f4/fonttools-4.63.0-py3-none-any.whl", hash = "sha256:445af2eab030a16b9171ea8bdda7ebf7d96bda2df88ee182a464252f6e05e20d", size = 1164562, upload-time = "2026-05-14T12:04:29.092Z" }, +] + +[package.optional-dependencies] +woff = [ + { name = "brotli", marker = "platform_python_implementation == 'CPython'" }, + { name = "brotlicffi", marker = "platform_python_implementation != 'CPython'" }, + { name = "zopfli" }, ] [[package]] @@ -974,39 +740,12 @@ wheels = [ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/6a/09/e21df6aef1e1ffc0c816f0522ddc3f6dcded766c3261813131c78a704470/gitpython-3.1.46-py3-none-any.whl", hash = "sha256:79812ed143d9d25b6d176a10bb511de0f9c67b1fa641d82097b0ab90398a2058", size = 208620, upload-time = "2026-01-01T15:37:30.574Z" }, ] -[[package]] -name = "graphviz" -version = "0.21" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f8/b3/3ac91e9be6b761a4b30d66ff165e54439dcd48b83f4e20d644867215f6ca/graphviz-0.21.tar.gz", hash = "sha256:20743e7183be82aaaa8ad6c93f8893c923bd6658a04c32ee115edb3c8a835f78", size = 200434, upload-time = "2025-06-15T09:35:05.824Z" } -wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/91/4c/e0ce1ef95d4000ebc1c11801f9b944fa5910ecc15b5e351865763d8657f8/graphviz-0.21-py3-none-any.whl", hash = "sha256:54f33de9f4f911d7e84e4191749cac8cc5653f815b06738c54db9a15ab8b1e42", size = 47300, upload-time = "2025-06-15T09:35:04.433Z" }, -] - [[package]] name = "greenlet" version = "3.1.1" source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/2f/ff/df5fede753cc10f6a5be0931204ea30c35fa2f2ea7a35b25bdaf4fe40e46/greenlet-3.1.1.tar.gz", hash = "sha256:4ce3ac6cdb6adf7946475d7ef31777c26d94bccc377e070a7986bd2d5c515467", size = 186022, upload-time = "2024-09-20T18:21:04.506Z" } wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/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://pypi.tuna.tsinghua.edu.cn/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://pypi.tuna.tsinghua.edu.cn/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://pypi.tuna.tsinghua.edu.cn/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://pypi.tuna.tsinghua.edu.cn/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://pypi.tuna.tsinghua.edu.cn/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://pypi.tuna.tsinghua.edu.cn/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" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e0/1d/a305dce121838d0278cee39d5bb268c657f10a5363ae4b726848f833f1bb/greenlet-3.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ed10eac5830befbdd0c32f83e8aa6288361597550ba669b04c48f0f9a2c843c6", size = 1149111, upload-time = "2024-09-20T17:09:22.104Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/96/28/d62835fb33fb5652f2e98d34c44ad1a0feacc8b1d3f1aecab035f51f267d/greenlet-3.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:77c386de38a60d1dfb8e55b8c1101d68c79dfdd25c7095d51fec2dd800892b80", size = 298392, upload-time = "2024-09-20T17:28:51.988Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/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://pypi.tuna.tsinghua.edu.cn/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://pypi.tuna.tsinghua.edu.cn/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://pypi.tuna.tsinghua.edu.cn/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://pypi.tuna.tsinghua.edu.cn/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://pypi.tuna.tsinghua.edu.cn/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://pypi.tuna.tsinghua.edu.cn/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" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f4/fb/201e1b932e584066e0f0658b538e73c459b34d44b4bd4034f682423bc801/greenlet-3.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1776fd7f989fc6b8d8c8cb8da1f6b82c5814957264d1f6cf818d475ec2bf6395", size = 1155198, upload-time = "2024-09-20T17:09:23.903Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/12/da/b9ed5e310bb8b89661b80cbcd4db5a067903bbcd7fc854923f5ebb4144f0/greenlet-3.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:48ca08c771c268a768087b408658e216133aecd835c0ded47ce955381105ba39", size = 298930, upload-time = "2024-09-20T17:25:18.656Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/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://pypi.tuna.tsinghua.edu.cn/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://pypi.tuna.tsinghua.edu.cn/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" }, @@ -1034,53 +773,6 @@ wheels = [ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ac/38/08cc303ddddc4b3d7c628c3039a61a3aae36c241ed01393d00c2fd663473/greenlet-3.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:411f015496fec93c1c8cd4e5238da364e1da7a124bcb293f085bf2860c32c6f6", size = 1142112, upload-time = "2024-09-20T17:09:28.753Z" }, ] -[[package]] -name = "griffe" -version = "2.0.2" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } -dependencies = [ - { name = "griffecli" }, - { name = "griffelib" }, -] -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/4a/49/eb6d2935e27883af92c930ed40cc4c69bcd32c402be43b8ca4ab20510f67/griffe-2.0.2.tar.gz", hash = "sha256:c5d56326d159f274492e9bf93a9895cec101155d944caa66d0fc4e0c13751b92", size = 293757, upload-time = "2026-03-27T11:34:52.205Z" } -wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/94/c0/2bb018eecf9a83c68db9cd9fffd9dab25f102ad30ed869451046e46d1187/griffe-2.0.2-py3-none-any.whl", hash = "sha256:2b31816460aee1996af26050a1fc6927a2e5936486856707f55508e4c9b5960b", size = 5141, upload-time = "2026-03-27T11:34:47.721Z" }, -] - -[[package]] -name = "griffecli" -version = "2.0.2" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } -dependencies = [ - { name = "colorama" }, - { name = "griffelib" }, -] -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/79/e0/6a7d661d71bb043656a109b91d84a42b5342752542074ec83b16a6eb97f0/griffecli-2.0.2.tar.gz", hash = "sha256:40a1ad4181fc39685d025e119ae2c5b669acdc1f19b705fb9bf971f4e6f6dffb", size = 56281, upload-time = "2026-03-27T11:34:50.087Z" } -wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/2e/e8/90d93356c88ac34c20cb5edffca68138df55ca9bbd1a06eccfbcec8fdbe5/griffecli-2.0.2-py3-none-any.whl", hash = "sha256:0d44d39e59afa81e288a3e1c3bf352cc4fa537483326ac06b8bb6a51fd8303a0", size = 9500, upload-time = "2026-03-27T11:34:48.81Z" }, -] - -[[package]] -name = "griffelib" -version = "2.0.2" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/9d/82/74f4a3310cdabfbb10da554c3a672847f1ed33c6f61dd472681ce7f1fe67/griffelib-2.0.2.tar.gz", hash = "sha256:3cf20b3bc470e83763ffbf236e0076b1211bac1bc67de13daf494640f2de707e", size = 166461, upload-time = "2026-03-27T11:34:51.091Z" } -wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/11/8c/c9138d881c79aa0ea9ed83cbd58d5ca75624378b38cee225dcf5c42cc91f/griffelib-2.0.2-py3-none-any.whl", hash = "sha256:925c857658fb1ba40c0772c37acbc2ab650bd794d9c1b9726922e36ea4117ea1", size = 142357, upload-time = "2026-03-27T11:34:46.275Z" }, -] - -[[package]] -name = "gunicorn" -version = "23.0.0" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } -dependencies = [ - { name = "packaging" }, -] -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/34/72/9614c465dc206155d93eff0ca20d42e1e35afc533971379482de953521a4/gunicorn-23.0.0.tar.gz", hash = "sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec", size = 375031, upload-time = "2024-08-10T20:25:27.378Z" } -wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/cb/7d/6dac2a6e1eba33ee43f318edbed4ff29151a49b5d37f080aad1e6469bca4/gunicorn-23.0.0-py3-none-any.whl", hash = "sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d", size = 85029, upload-time = "2024-08-10T20:25:24.996Z" }, -] - [[package]] name = "h11" version = "0.16.0" @@ -1146,15 +838,6 @@ http2 = [ { name = "h2" }, ] -[[package]] -name = "humanize" -version = "4.15.0" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ba/66/a3921783d54be8a6870ac4ccffcd15c4dc0dd7fcce51c6d63b8c63935276/humanize-4.15.0.tar.gz", hash = "sha256:1dd098483eb1c7ee8e32eb2e99ad1910baefa4b75c3aff3a82f4d78688993b10", size = 83599, upload-time = "2025-12-20T20:16:13.19Z" } -wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c5/7b/bca5613a0c3b542420cf92bd5e5fb8ebd5435ce1011a091f66bb7693285e/humanize-4.15.0-py3-none-any.whl", hash = "sha256:b1186eb9f5a9749cd9cb8565aee77919dd7c8d076161cf44d70e59e3301e1769", size = 132203, upload-time = "2025-12-20T20:16:11.67Z" }, -] - [[package]] name = "hyperframe" version = "6.1.0" @@ -1173,18 +856,6 @@ wheels = [ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, ] -[[package]] -name = "importlib-metadata" -version = "8.7.1" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } -dependencies = [ - { name = "zipp" }, -] -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f3/49/3b30cad09e7771a4982d9975a8cbf64f00d4a1ececb53297f1d9a7be1b10/importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb", size = 57107, upload-time = "2025-12-21T10:00:19.278Z" } -wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" }, -] - [[package]] name = "iniconfig" version = "2.3.0" @@ -1215,50 +886,12 @@ wheels = [ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, ] -[[package]] -name = "jinja2-humanize-extension" -version = "0.4.0" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } -dependencies = [ - { name = "humanize" }, - { name = "jinja2" }, -] -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/74/77/0bba383819dd4e67566487c11c49479ced87e77c3285d8e7f7a3401cf882/jinja2_humanize_extension-0.4.0.tar.gz", hash = "sha256:e7d69b1c20f32815bbec722330ee8af14b1287bb1c2b0afa590dbf031cadeaa0" } -wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/26/b4/08c9d297edd5e1182506edecccbb88a92e1122a057953068cadac420ca5d/jinja2_humanize_extension-0.4.0-py3-none-any.whl", hash = "sha256:b6326e2da0f7d425338bebf58848e830421defbce785f12ae812e65128518156" }, -] - [[package]] name = "jiter" version = "0.12.0" source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/45/9d/e0660989c1370e25848bb4c52d061c71837239738ad937e83edca174c273/jiter-0.12.0.tar.gz", hash = "sha256:64dfcd7d5c168b38d3f9f8bba7fc639edb3418abcc74f22fdbe6b8938293f30b", size = 168294, upload-time = "2025-11-09T20:49:23.302Z" } wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/3b/91/13cb9505f7be74a933f37da3af22e029f6ba64f5669416cb8b2774bc9682/jiter-0.12.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:e7acbaba9703d5de82a2c98ae6a0f59ab9770ab5af5fa35e43a303aee962cf65", size = 316652, upload-time = "2025-11-09T20:46:41.021Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/4e/76/4e9185e5d9bb4e482cf6dec6410d5f78dfeb374cfcecbbe9888d07c52daa/jiter-0.12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:364f1a7294c91281260364222f535bc427f56d4de1d8ffd718162d21fbbd602e", size = 319829, upload-time = "2025-11-09T20:46:43.281Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/86/af/727de50995d3a153138139f259baae2379d8cb0522c0c00419957bc478a6/jiter-0.12.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85ee4d25805d4fb23f0a5167a962ef8e002dbfb29c0989378488e32cf2744b62", size = 350568, upload-time = "2025-11-09T20:46:45.075Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/6a/c1/d6e9f4b7a3d5ac63bcbdfddeb50b2dcfbdc512c86cffc008584fdc350233/jiter-0.12.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:796f466b7942107eb889c08433b6e31b9a7ed31daceaecf8af1be26fb26c0ca8", size = 369052, upload-time = "2025-11-09T20:46:46.818Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/eb/be/00824cd530f30ed73fa8a4f9f3890a705519e31ccb9e929f1e22062e7c76/jiter-0.12.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:35506cb71f47dba416694e67af996bbdefb8e3608f1f78799c2e1f9058b01ceb", size = 481585, upload-time = "2025-11-09T20:46:48.319Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/74/b6/2ad7990dff9504d4b5052eef64aa9574bd03d722dc7edced97aad0d47be7/jiter-0.12.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:726c764a90c9218ec9e4f99a33d6bf5ec169163f2ca0fc21b654e88c2abc0abc", size = 380541, upload-time = "2025-11-09T20:46:49.643Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b5/c7/f3c26ecbc1adbf1db0d6bba99192143d8fe8504729d9594542ecc4445784/jiter-0.12.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa47810c5565274810b726b0dc86d18dce5fd17b190ebdc3890851d7b2a0e74", size = 364423, upload-time = "2025-11-09T20:46:51.731Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/18/51/eac547bf3a2d7f7e556927278e14c56a0604b8cddae75815d5739f65f81d/jiter-0.12.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f8ec0259d3f26c62aed4d73b198c53e316ae11f0f69c8fbe6682c6dcfa0fcce2", size = 389958, upload-time = "2025-11-09T20:46:53.432Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/2c/1f/9ca592e67175f2db156cff035e0d817d6004e293ee0c1d73692d38fcb596/jiter-0.12.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:79307d74ea83465b0152fa23e5e297149506435535282f979f18b9033c0bb025", size = 522084, upload-time = "2025-11-09T20:46:54.848Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/83/ff/597d9cdc3028f28224f53e1a9d063628e28b7a5601433e3196edda578cdd/jiter-0.12.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:cf6e6dd18927121fec86739f1a8906944703941d000f0639f3eb6281cc601dca", size = 513054, upload-time = "2025-11-09T20:46:56.487Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/24/6d/1970bce1351bd02e3afcc5f49e4f7ef3dabd7fb688f42be7e8091a5b809a/jiter-0.12.0-cp310-cp310-win32.whl", hash = "sha256:b6ae2aec8217327d872cbfb2c1694489057b9433afce447955763e6ab015b4c4", size = 206368, upload-time = "2025-11-09T20:46:58.638Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e3/6b/eb1eb505b2d86709b59ec06681a2b14a94d0941db091f044b9f0e16badc0/jiter-0.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:c7f49ce90a71e44f7e1aa9e7ec415b9686bbc6a5961e57eab511015e6759bc11", size = 204847, upload-time = "2025-11-09T20:47:00.295Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/32/f9/eaca4633486b527ebe7e681c431f529b63fe2709e7c5242fc0f43f77ce63/jiter-0.12.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d8f8a7e317190b2c2d60eb2e8aa835270b008139562d70fe732e1c0020ec53c9", size = 316435, upload-time = "2025-11-09T20:47:02.087Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/10/c1/40c9f7c22f5e6ff715f28113ebaba27ab85f9af2660ad6e1dd6425d14c19/jiter-0.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2218228a077e784c6c8f1a8e5d6b8cb1dea62ce25811c356364848554b2056cd", size = 320548, upload-time = "2025-11-09T20:47:03.409Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/6b/1b/efbb68fe87e7711b00d2cfd1f26bb4bfc25a10539aefeaa7727329ffb9cb/jiter-0.12.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9354ccaa2982bf2188fd5f57f79f800ef622ec67beb8329903abf6b10da7d423", size = 351915, upload-time = "2025-11-09T20:47:05.171Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/15/2d/c06e659888c128ad1e838123d0638f0efad90cc30860cb5f74dd3f2fc0b3/jiter-0.12.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8f2607185ea89b4af9a604d4c7ec40e45d3ad03ee66998b031134bc510232bb7", size = 368966, upload-time = "2025-11-09T20:47:06.508Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/6b/20/058db4ae5fb07cf6a4ab2e9b9294416f606d8e467fb74c2184b2a1eeacba/jiter-0.12.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3a585a5e42d25f2e71db5f10b171f5e5ea641d3aa44f7df745aa965606111cc2", size = 482047, upload-time = "2025-11-09T20:47:08.382Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/49/bb/dc2b1c122275e1de2eb12905015d61e8316b2f888bdaac34221c301495d6/jiter-0.12.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd9e21d34edff5a663c631f850edcb786719c960ce887a5661e9c828a53a95d9", size = 380835, upload-time = "2025-11-09T20:47:09.81Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/23/7d/38f9cd337575349de16da575ee57ddb2d5a64d425c9367f5ef9e4612e32e/jiter-0.12.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a612534770470686cd5431478dc5a1b660eceb410abade6b1b74e320ca98de6", size = 364587, upload-time = "2025-11-09T20:47:11.529Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f0/a3/b13e8e61e70f0bb06085099c4e2462647f53cc2ca97614f7fedcaa2bb9f3/jiter-0.12.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3985aea37d40a908f887b34d05111e0aae822943796ebf8338877fee2ab67725", size = 390492, upload-time = "2025-11-09T20:47:12.993Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/07/71/e0d11422ed027e21422f7bc1883c61deba2d9752b720538430c1deadfbca/jiter-0.12.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b1207af186495f48f72529f8d86671903c8c10127cac6381b11dddc4aaa52df6", size = 522046, upload-time = "2025-11-09T20:47:14.6Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/9f/59/b968a9aa7102a8375dbbdfbd2aeebe563c7e5dddf0f47c9ef1588a97e224/jiter-0.12.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ef2fb241de583934c9915a33120ecc06d94aa3381a134570f59eed784e87001e", size = 513392, upload-time = "2025-11-09T20:47:16.011Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ca/e4/7df62002499080dbd61b505c5cb351aa09e9959d176cac2aa8da6f93b13b/jiter-0.12.0-cp311-cp311-win32.whl", hash = "sha256:453b6035672fecce8007465896a25b28a6b59cfe8fbc974b2563a92f5a92a67c", size = 206096, upload-time = "2025-11-09T20:47:17.344Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/bb/60/1032b30ae0572196b0de0e87dce3b6c26a1eff71aad5fe43dee3082d32e0/jiter-0.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:ca264b9603973c2ad9435c71a8ec8b49f8f715ab5ba421c85a51cde9887e421f", size = 204899, upload-time = "2025-11-09T20:47:19.365Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/49/d5/c145e526fccdb834063fb45c071df78b0cc426bbaf6de38b0781f45d956f/jiter-0.12.0-cp311-cp311-win_arm64.whl", hash = "sha256:cb00ef392e7d684f2754598c02c409f376ddcef857aae796d559e6cacc2d78a5", size = 188070, upload-time = "2025-11-09T20:47:20.75Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/92/c9/5b9f7b4983f1b542c64e84165075335e8a236fa9e2ea03a0c79780062be8/jiter-0.12.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:305e061fa82f4680607a775b2e8e0bcb071cd2205ac38e6ef48c8dd5ebe1cf37", size = 314449, upload-time = "2025-11-09T20:47:22.999Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/98/6e/e8efa0e78de00db0aee82c0cf9e8b3f2027efd7f8a71f859d8f4be8e98ef/jiter-0.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5c1860627048e302a528333c9307c818c547f214d8659b0705d2195e1a94b274", size = 319855, upload-time = "2025-11-09T20:47:24.779Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/20/26/894cd88e60b5d58af53bec5c6759d1292bd0b37a8b5f60f07abf7a63ae5f/jiter-0.12.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df37577a4f8408f7e0ec3205d2a8f87672af8f17008358063a4d6425b6081ce3", size = 350171, upload-time = "2025-11-09T20:47:26.469Z" }, @@ -1315,64 +948,12 @@ wheels = [ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/aa/51/2cb4468b3448a8385ebcd15059d325c9ce67df4e2758d133ab9442b19834/jiter-0.12.0-cp314-cp314t-win32.whl", hash = "sha256:8bbcfe2791dfdb7c5e48baf646d37a6a3dcb5a97a032017741dea9f817dca183", size = 205110, upload-time = "2025-11-09T20:48:47.033Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b2/c5/ae5ec83dec9c2d1af805fd5fe8f74ebded9c8670c5210ec7820ce0dbeb1e/jiter-0.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2fa940963bf02e1d8226027ef461e36af472dea85d36054ff835aeed944dd873", size = 205223, upload-time = "2025-11-09T20:48:49.076Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/97/9a/3c5391907277f0e55195550cf3fa8e293ae9ee0c00fb402fec1e38c0c82f/jiter-0.12.0-cp314-cp314t-win_arm64.whl", hash = "sha256:506c9708dd29b27288f9f8f1140c3cb0e3d8ddb045956d7757b1fa0e0f39a473", size = 185564, upload-time = "2025-11-09T20:48:50.376Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/fe/54/5339ef1ecaa881c6948669956567a64d2670941925f245c434f494ffb0e5/jiter-0.12.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:4739a4657179ebf08f85914ce50332495811004cc1747852e8b2041ed2aab9b8", size = 311144, upload-time = "2025-11-09T20:49:10.503Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/27/74/3446c652bffbd5e81ab354e388b1b5fc1d20daac34ee0ed11ff096b1b01a/jiter-0.12.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:41da8def934bf7bec16cb24bd33c0ca62126d2d45d81d17b864bd5ad721393c3", size = 305877, upload-time = "2025-11-09T20:49:12.269Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a1/f4/ed76ef9043450f57aac2d4fbeb27175aa0eb9c38f833be6ef6379b3b9a86/jiter-0.12.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c44ee814f499c082e69872d426b624987dbc5943ab06e9bbaa4f81989fdb79e", size = 340419, upload-time = "2025-11-09T20:49:13.803Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/21/01/857d4608f5edb0664aa791a3d45702e1a5bcfff9934da74035e7b9803846/jiter-0.12.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cd2097de91cf03eaa27b3cbdb969addf83f0179c6afc41bbc4513705e013c65d", size = 347212, upload-time = "2025-11-09T20:49:15.643Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/cb/f5/12efb8ada5f5c9edc1d4555fe383c1fb2eac05ac5859258a72d61981d999/jiter-0.12.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:e8547883d7b96ef2e5fe22b88f8a4c8725a56e7f4abafff20fd5272d634c7ecb", size = 309974, upload-time = "2025-11-09T20:49:17.187Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/85/15/d6eb3b770f6a0d332675141ab3962fd4a7c270ede3515d9f3583e1d28276/jiter-0.12.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:89163163c0934854a668ed783a2546a0617f71706a2551a4a0666d91ab365d6b", size = 304233, upload-time = "2025-11-09T20:49:18.734Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/8c/3e/e7e06743294eea2cf02ced6aa0ff2ad237367394e37a0e2b4a1108c67a36/jiter-0.12.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d96b264ab7d34bbb2312dedc47ce07cd53f06835eacbc16dde3761f47c3a9e7f", size = 338537, upload-time = "2025-11-09T20:49:20.317Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/2f/9c/6753e6522b8d0ef07d3a3d239426669e984fb0eba15a315cdbc1253904e4/jiter-0.12.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c24e864cb30ab82311c6425655b0cdab0a98c5d973b065c66a3f020740c2324c", size = 346110, upload-time = "2025-11-09T20:49:21.817Z" }, ] -[[package]] -name = "jsonpatch" -version = "1.33" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } -dependencies = [ - { name = "jsonpointer" }, -] -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/42/78/18813351fe5d63acad16aec57f94ec2b70a09e53ca98145589e185423873/jsonpatch-1.33.tar.gz", hash = "sha256:9fcd4009c41e6d12348b4a0ff2563ba56a2923a7dfee731d004e212e1ee5030c" } -wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/73/07/02e16ed01e04a374e644b575638ec7987ae846d25ad97bcc9945a3ee4b0e/jsonpatch-1.33-py2.py3-none-any.whl", hash = "sha256:0ae28c0cd062bbd8b8ecc26d7d164fbbea9652a1a3693f3b956c1eae5145dade" }, -] - -[[package]] -name = "jsonpointer" -version = "3.0.0" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/6a/0a/eebeb1fa92507ea94016a2a790b93c2ae41a7e18778f85471dc54475ed25/jsonpointer-3.0.0.tar.gz", hash = "sha256:2b2d729f2091522d61c3b31f82e11870f60b68f43fbc705cb76bf4b832af59ef", size = 9114, upload-time = "2024-06-10T19:24:42.462Z" } -wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/71/92/5e77f98553e9e75130c78900d000368476aed74276eb8ae8796f65f00918/jsonpointer-3.0.0-py2.py3-none-any.whl", hash = "sha256:13e088adc14fca8b6aa8177c044e12701e6ad4b28ff10e65f2267a90109c9942", size = 7595, upload-time = "2024-06-10T19:24:40.698Z" }, -] - -[[package]] -name = "jsonschema" -version = "4.26.0" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } -dependencies = [ - { name = "attrs" }, - { name = "jsonschema-specifications" }, - { name = "referencing" }, - { name = "rpds-py" }, -] -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" } -wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" }, -] - -[[package]] -name = "jsonschema-specifications" -version = "2025.9.1" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } -dependencies = [ - { name = "referencing" }, -] -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } -wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, -] - [[package]] name = "loguru" version = "0.7.3" @@ -1386,80 +967,6 @@ wheels = [ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/0c/29/0348de65b8cc732daa3e33e67806420b2ae89bdce2b04af740289c5c6c8c/loguru-0.7.3-py3-none-any.whl", hash = "sha256:31a33c10c8e1e10422bfd431aeb5d351c7cf7fa671e3c4df004162264b28220c", size = 61595, upload-time = "2024-12-06T11:20:54.538Z" }, ] -[[package]] -name = "lupa" -version = "2.6" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b8/1c/191c3e6ec6502e3dbe25a53e27f69a5daeac3e56de1f73c0138224171ead/lupa-2.6.tar.gz", hash = "sha256:9a770a6e89576be3447668d7ced312cd6fd41d3c13c2462c9dc2c2ab570e45d9", size = 7240282, upload-time = "2025-10-24T07:20:29.738Z" } -wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a1/15/713cab5d0dfa4858f83b99b3e0329072df33dc14fc3ebbaa017e0f9755c4/lupa-2.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6b3dabda836317e63c5ad052826e156610f356a04b3003dfa0dbe66b5d54d671", size = 954828, upload-time = "2025-10-24T07:17:15.726Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/2e/71/704740cbc6e587dd6cc8dabf2f04820ac6a671784e57cc3c29db795476db/lupa-2.6-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:8726d1c123bbe9fbb974ce29825e94121824e66003038ff4532c14cc2ed0c51c", size = 1919259, upload-time = "2025-10-24T07:17:18.586Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/eb/18/f248341c423c5d48837e35584c6c3eb4acab7e722b6057d7b3e28e42dae8/lupa-2.6-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:f4e159e7d814171199b246f9235ca8961f6461ea8c1165ab428afa13c9289a94", size = 984998, upload-time = "2025-10-24T07:17:20.428Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/44/1e/8a4bd471e018aad76bcb9455d298c2c96d82eced20f2ae8fcec8cd800948/lupa-2.6-cp310-cp310-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:202160e80dbfddfb79316692a563d843b767e0f6787bbd1c455f9d54052efa6c", size = 1174871, upload-time = "2025-10-24T07:17:22.755Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/2a/5c/3a3f23fd6a91b0986eea1ceaf82ad3f9b958fe3515a9981fb9c4eb046c8b/lupa-2.6-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5deede7c5b36ab64f869dae4831720428b67955b0bb186c8349cf6ea121c852b", size = 1057471, upload-time = "2025-10-24T07:17:24.908Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/45/ac/01be1fed778fb0c8f46ee8cbe344e4d782f6806fac12717f08af87aa4355/lupa-2.6-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:86f04901f920bbf7c0cac56807dc9597e42347123e6f1f3ca920f15f54188ce5", size = 2100592, upload-time = "2025-10-24T07:17:27.089Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/3f/6c/1a05bb873e30830f8574e10cd0b4cdbc72e9dbad2a09e25810b5e3b1f75d/lupa-2.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6deef8f851d6afb965c84849aa5b8c38856942df54597a811ce0369ced678610", size = 1081396, upload-time = "2025-10-24T07:17:29.064Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a2/c2/a19dd80d6dc98b39bbf8135b8198e38aa7ca3360b720eac68d1d7e9286b5/lupa-2.6-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:21f2b5549681c2a13b1170a26159d30875d367d28f0247b81ca347222c755038", size = 1192007, upload-time = "2025-10-24T07:17:31.362Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/4f/43/e1b297225c827f55752e46fdbfb021c8982081b0f24490e42776ea69ae3b/lupa-2.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:66eea57630eab5e6f49fdc5d7811c0a2a41f2011be4ea56a087ea76112011eb7", size = 2196661, upload-time = "2025-10-24T07:17:33.484Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/2e/8f/2272d429a7fa9dc8dbd6e9c5c9073a03af6007eb22a4c78829fec6a34b80/lupa-2.6-cp310-cp310-win32.whl", hash = "sha256:60a403de8cab262a4fe813085dd77010effa6e2eb1886db2181df803140533b1", size = 1412738, upload-time = "2025-10-24T07:17:35.11Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/35/2a/1708911271dd49ad87b4b373b5a4b0e0a0516d3d2af7b76355946c7ee171/lupa-2.6-cp310-cp310-win_amd64.whl", hash = "sha256:e4656a39d93dfa947cf3db56dc16c7916cb0cc8024acd3a952071263f675df64", size = 1656898, upload-time = "2025-10-24T07:17:36.949Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ca/29/1f66907c1ebf1881735afa695e646762c674f00738ebf66d795d59fc0665/lupa-2.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6d988c0f9331b9f2a5a55186701a25444ab10a1432a1021ee58011499ecbbdd5", size = 962875, upload-time = "2025-10-24T07:17:39.107Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e6/67/4a748604be360eb9c1c215f6a0da921cd1a2b44b2c5951aae6fb83019d3a/lupa-2.6-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:ebe1bbf48259382c72a6fe363dea61a0fd6fe19eab95e2ae881e20f3654587bf", size = 1935390, upload-time = "2025-10-24T07:17:41.427Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ac/0c/8ef9ee933a350428b7bdb8335a37ef170ab0bb008bbf9ca8f4f4310116b6/lupa-2.6-cp311-cp311-macosx_11_0_x86_64.whl", hash = "sha256:a8fcee258487cf77cdd41560046843bb38c2e18989cd19671dd1e2596f798306", size = 992193, upload-time = "2025-10-24T07:17:43.231Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/65/46/e6c7facebdb438db8a65ed247e56908818389c1a5abbf6a36aab14f1057d/lupa-2.6-cp311-cp311-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:561a8e3be800827884e767a694727ed8482d066e0d6edfcbf423b05e63b05535", size = 1165844, upload-time = "2025-10-24T07:17:45.437Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/1c/26/9f1154c6c95f175ccbf96aa96c8f569c87f64f463b32473e839137601a8b/lupa-2.6-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:af880a62d47991cae78b8e9905c008cbfdc4a3a9723a66310c2634fc7644578c", size = 1048069, upload-time = "2025-10-24T07:17:47.181Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/68/67/2cc52ab73d6af81612b2ea24c870d3fa398443af8e2875e5befe142398b1/lupa-2.6-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:80b22923aa4023c86c0097b235615f89d469a0c4eee0489699c494d3367c4c85", size = 2079079, upload-time = "2025-10-24T07:17:49.755Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/2e/dc/f843f09bbf325f6e5ee61730cf6c3409fc78c010d968c7c78acba3019ca7/lupa-2.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:153d2cc6b643f7efb9cfc0c6bb55ec784d5bac1a3660cfc5b958a7b8f38f4a75", size = 1071428, upload-time = "2025-10-24T07:17:51.991Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/2e/60/37533a8d85bf004697449acb97ecdacea851acad28f2ad3803662487dd2a/lupa-2.6-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:3fa8777e16f3ded50b72967dc17e23f5a08e4f1e2c9456aff2ebdb57f5b2869f", size = 1181756, upload-time = "2025-10-24T07:17:53.752Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e4/f2/cf29b20dbb4927b6a3d27c339ac5d73e74306ecc28c8e2c900b2794142ba/lupa-2.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8dbdcbe818c02a2f56f5ab5ce2de374dab03e84b25266cfbaef237829bc09b3f", size = 2175687, upload-time = "2025-10-24T07:17:56.228Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/94/7c/050e02f80c7131b63db1474bff511e63c545b5a8636a24cbef3fc4da20b6/lupa-2.6-cp311-cp311-win32.whl", hash = "sha256:defaf188fde8f7a1e5ce3a5e6d945e533b8b8d547c11e43b96c9b7fe527f56dc", size = 1412592, upload-time = "2025-10-24T07:17:59.062Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/6f/9a/6f2af98aa5d771cea661f66c8eb8f53772ec1ab1dfbce24126cfcd189436/lupa-2.6-cp311-cp311-win_amd64.whl", hash = "sha256:9505ae600b5c14f3e17e70f87f88d333717f60411faca1ddc6f3e61dce85fa9e", size = 1669194, upload-time = "2025-10-24T07:18:01.647Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/94/86/ce243390535c39d53ea17ccf0240815e6e457e413e40428a658ea4ee4b8d/lupa-2.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:47ce718817ef1cc0c40d87c3d5ae56a800d61af00fbc0fad1ca9be12df2f3b56", size = 951707, upload-time = "2025-10-24T07:18:03.884Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/86/85/cedea5e6cbeb54396fdcc55f6b741696f3f036d23cfaf986d50d680446da/lupa-2.6-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:7aba985b15b101495aa4b07112cdc08baa0c545390d560ad5cfde2e9e34f4d58", size = 1916703, upload-time = "2025-10-24T07:18:05.6Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/24/be/3d6b5f9a8588c01a4d88129284c726017b2089f3a3fd3ba8bd977292fea0/lupa-2.6-cp312-cp312-macosx_11_0_x86_64.whl", hash = "sha256:b766f62f95b2739f2248977d29b0722e589dcf4f0ccfa827ccbd29f0148bd2e5", size = 985152, upload-time = "2025-10-24T07:18:08.561Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/eb/23/9f9a05beee5d5dce9deca4cb07c91c40a90541fc0a8e09db4ee670da550f/lupa-2.6-cp312-cp312-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:00a934c23331f94cb51760097ebfab14b005d55a6b30a2b480e3c53dd2fa290d", size = 1159599, upload-time = "2025-10-24T07:18:10.346Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/40/4e/e7c0583083db9d7f1fd023800a9767d8e4391e8330d56c2373d890ac971b/lupa-2.6-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21de9f38bd475303e34a042b7081aabdf50bd9bafd36ce4faea2f90fd9f15c31", size = 1038686, upload-time = "2025-10-24T07:18:12.112Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/1c/9f/5a4f7d959d4feba5e203ff0c31889e74d1ca3153122be4a46dca7d92bf7c/lupa-2.6-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf3bda96d3fc41237e964a69c23647d50d4e28421111360274d4799832c560e9", size = 2071956, upload-time = "2025-10-24T07:18:14.572Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/92/34/2f4f13ca65d01169b1720176aedc4af17bc19ee834598c7292db232cb6dc/lupa-2.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5a76ead245da54801a81053794aa3975f213221f6542d14ec4b859ee2e7e0323", size = 1057199, upload-time = "2025-10-24T07:18:16.379Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/35/2a/5f7d2eebec6993b0dcd428e0184ad71afb06a45ba13e717f6501bfed1da3/lupa-2.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8dd0861741caa20886ddbda0a121d8e52fb9b5bb153d82fa9bba796962bf30e8", size = 1173693, upload-time = "2025-10-24T07:18:18.153Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e4/29/089b4d2f8e34417349af3904bb40bec40b65c8731f45e3fd8d497ca573e5/lupa-2.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:239e63948b0b23023f81d9a19a395e768ed3da6a299f84e7963b8f813f6e3f9c", size = 2164394, upload-time = "2025-10-24T07:18:20.403Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f3/1b/79c17b23c921f81468a111cad843b076a17ef4b684c4a8dff32a7969c3f0/lupa-2.6-cp312-cp312-win32.whl", hash = "sha256:325894e1099499e7a6f9c351147661a2011887603c71086d36fe0f964d52d1ce", size = 1420647, upload-time = "2025-10-24T07:18:23.368Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b8/15/5121e68aad3584e26e1425a5c9a79cd898f8a152292059e128c206ee817c/lupa-2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c735a1ce8ee60edb0fe71d665f1e6b7c55c6021f1d340eb8c865952c602cd36f", size = 1688529, upload-time = "2025-10-24T07:18:25.523Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/28/1d/21176b682ca5469001199d8b95fa1737e29957a3d185186e7a8b55345f2e/lupa-2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:663a6e58a0f60e7d212017d6678639ac8df0119bc13c2145029dcba084391310", size = 947232, upload-time = "2025-10-24T07:18:27.878Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ce/4c/d327befb684660ca13cf79cd1f1d604331808f9f1b6fb6bf57832f8edf80/lupa-2.6-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:d1f5afda5c20b1f3217a80e9bc1b77037f8a6eb11612fd3ada19065303c8f380", size = 1908625, upload-time = "2025-10-24T07:18:29.944Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/66/8e/ad22b0a19454dfd08662237a84c792d6d420d36b061f239e084f29d1a4f3/lupa-2.6-cp313-cp313-macosx_11_0_x86_64.whl", hash = "sha256:26f2b3c085fe76e9119e48c1013c1cccdc1f51585d456858290475aa38e7089e", size = 981057, upload-time = "2025-10-24T07:18:31.553Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/5c/48/74859073ab276bd0566c719f9ca0108b0cfc1956ca0d68678d117d47d155/lupa-2.6-cp313-cp313-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:60d2f902c7b96fb8ab98493dcff315e7bb4d0b44dc9dd76eb37de575025d5685", size = 1156227, upload-time = "2025-10-24T07:18:33.981Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/09/6c/0e9ded061916877253c2266074060eb71ed99fb21d73c8c114a76725bce2/lupa-2.6-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a02d25dee3a3250967c36590128d9220ae02f2eda166a24279da0b481519cbff", size = 1035752, upload-time = "2025-10-24T07:18:36.32Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/dd/ef/f8c32e454ef9f3fe909f6c7d57a39f950996c37a3deb7b391fec7903dab7/lupa-2.6-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6eae1ee16b886b8914ff292dbefbf2f48abfbdee94b33a88d1d5475e02423203", size = 2069009, upload-time = "2025-10-24T07:18:38.072Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/53/dc/15b80c226a5225815a890ee1c11f07968e0aba7a852df41e8ae6fe285063/lupa-2.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b0edd5073a4ee74ab36f74fe61450148e6044f3952b8d21248581f3c5d1a58be", size = 1056301, upload-time = "2025-10-24T07:18:40.165Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/31/14/2086c1425c985acfb30997a67e90c39457122df41324d3c179d6ee2292c6/lupa-2.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:0c53ee9f22a8a17e7d4266ad48e86f43771951797042dd51d1494aaa4f5f3f0a", size = 1170673, upload-time = "2025-10-24T07:18:42.426Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/10/e5/b216c054cf86576c0191bf9a9f05de6f7e8e07164897d95eea0078dca9b2/lupa-2.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:de7c0f157a9064a400d828789191a96da7f4ce889969a588b87ec80de9b14772", size = 2162227, upload-time = "2025-10-24T07:18:46.112Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/59/2f/33ecb5bedf4f3bc297ceacb7f016ff951331d352f58e7e791589609ea306/lupa-2.6-cp313-cp313-win32.whl", hash = "sha256:ee9523941ae0a87b5b703417720c5d78f72d2f5bc23883a2ea80a949a3ed9e75", size = 1419558, upload-time = "2025-10-24T07:18:48.371Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f9/b4/55e885834c847ea610e111d87b9ed4768f0afdaeebc00cd46810f25029f6/lupa-2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b1335a5835b0a25ebdbc75cf0bda195e54d133e4d994877ef025e218c2e59db9", size = 1683424, upload-time = "2025-10-24T07:18:50.976Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/66/9d/d9427394e54d22a35d1139ef12e845fd700d4872a67a34db32516170b746/lupa-2.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:dcb6d0a3264873e1653bc188499f48c1fb4b41a779e315eba45256cfe7bc33c1", size = 953818, upload-time = "2025-10-24T07:18:53.378Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/10/41/27bbe81953fb2f9ecfced5d9c99f85b37964cfaf6aa8453bb11283983721/lupa-2.6-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:a37e01f2128f8c36106726cb9d360bac087d58c54b4522b033cc5691c584db18", size = 1915850, upload-time = "2025-10-24T07:18:55.259Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a3/98/f9ff60db84a75ba8725506bbf448fb085bc77868a021998ed2a66d920568/lupa-2.6-cp314-cp314-macosx_11_0_x86_64.whl", hash = "sha256:458bd7e9ff3c150b245b0fcfbb9bd2593d1152ea7f0a7b91c1d185846da033fe", size = 982344, upload-time = "2025-10-24T07:18:57.05Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/41/f7/f39e0f1c055c3b887d86b404aaf0ca197b5edfd235a8b81b45b25bac7fc3/lupa-2.6-cp314-cp314-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:052ee82cac5206a02df77119c325339acbc09f5ce66967f66a2e12a0f3211cad", size = 1156543, upload-time = "2025-10-24T07:18:59.251Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/9e/9c/59e6cffa0d672d662ae17bd7ac8ecd2c89c9449dee499e3eb13ca9cd10d9/lupa-2.6-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96594eca3c87dd07938009e95e591e43d554c1dbd0385be03c100367141db5a8", size = 1047974, upload-time = "2025-10-24T07:19:01.449Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/23/c6/a04e9cef7c052717fcb28fb63b3824802488f688391895b618e39be0f684/lupa-2.6-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8faddd9d198688c8884091173a088a8e920ecc96cda2ffed576a23574c4b3f6", size = 2073458, upload-time = "2025-10-24T07:19:03.369Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e6/10/824173d10f38b51fc77785228f01411b6ca28826ce27404c7c912e0e442c/lupa-2.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:daebb3a6b58095c917e76ba727ab37b27477fb926957c825205fbda431552134", size = 1067683, upload-time = "2025-10-24T07:19:06.2Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b6/dc/9692fbcf3c924d9c4ece2d8d2f724451ac2e09af0bd2a782db1cef34e799/lupa-2.6-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:f3154e68972befe0f81564e37d8142b5d5d79931a18309226a04ec92487d4ea3", size = 1171892, upload-time = "2025-10-24T07:19:08.544Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/84/ff/e318b628d4643c278c96ab3ddea07fc36b075a57383c837f5b11e537ba9d/lupa-2.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e4dadf77b9fedc0bfa53417cc28dc2278a26d4cbd95c29f8927ad4d8fe0a7ef9", size = 2166641, upload-time = "2025-10-24T07:19:10.485Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/12/f7/a6f9ec2806cf2d50826980cdb4b3cffc7691dc6f95e13cc728846d5cb793/lupa-2.6-cp314-cp314-win32.whl", hash = "sha256:cb34169c6fa3bab3e8ac58ca21b8a7102f6a94b6a5d08d3636312f3f02fafd8f", size = 1456857, upload-time = "2025-10-24T07:19:37.989Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c5/de/df71896f25bdc18360fdfa3b802cd7d57d7fede41a0e9724a4625b412c85/lupa-2.6-cp314-cp314-win_amd64.whl", hash = "sha256:b74f944fe46c421e25d0f8692aef1e842192f6f7f68034201382ac440ef9ea67", size = 1731191, upload-time = "2025-10-24T07:19:40.281Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/47/3c/a1f23b01c54669465f5f4c4083107d496fbe6fb45998771420e9aadcf145/lupa-2.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0e21b716408a21ab65723f8841cf7f2f37a844b7a965eeabb785e27fca4099cf", size = 999343, upload-time = "2025-10-24T07:19:12.519Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c5/6d/501994291cb640bfa2ccf7f554be4e6914afa21c4026bd01bff9ca8aac57/lupa-2.6-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:589db872a141bfff828340079bbdf3e9a31f2689f4ca0d88f97d9e8c2eae6142", size = 2000730, upload-time = "2025-10-24T07:19:14.869Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/53/a5/457ffb4f3f20469956c2d4c4842a7675e884efc895b2f23d126d23e126cc/lupa-2.6-cp314-cp314t-macosx_11_0_x86_64.whl", hash = "sha256:cd852a91a4a9d4dcbb9a58100f820a75a425703ec3e3f049055f60b8533b7953", size = 1021553, upload-time = "2025-10-24T07:19:17.123Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/51/6b/36bb5a5d0960f2a5c7c700e0819abb76fd9bf9c1d8a66e5106416d6e9b14/lupa-2.6-cp314-cp314t-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:0334753be028358922415ca97a64a3048e4ed155413fc4eaf87dd0a7e2752983", size = 1133275, upload-time = "2025-10-24T07:19:20.51Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/19/86/202ff4429f663013f37d2229f6176ca9f83678a50257d70f61a0a97281bf/lupa-2.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:661d895cd38c87658a34780fac54a690ec036ead743e41b74c3fb81a9e65a6aa", size = 1038441, upload-time = "2025-10-24T07:19:22.509Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a7/42/d8125f8e420714e5b52e9c08d88b5329dfb02dcca731b4f21faaee6cc5b5/lupa-2.6-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6aa58454ccc13878cc177c62529a2056be734da16369e451987ff92784994ca7", size = 2058324, upload-time = "2025-10-24T07:19:24.979Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/2b/2c/47bf8b84059876e877a339717ddb595a4a7b0e8740bacae78ba527562e1c/lupa-2.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1425017264e470c98022bba8cff5bd46d054a827f5df6b80274f9cc71dafd24f", size = 1060250, upload-time = "2025-10-24T07:19:27.262Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c2/06/d88add2b6406ca1bdec99d11a429222837ca6d03bea42ca75afa169a78cb/lupa-2.6-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:224af0532d216e3105f0a127410f12320f7c5f1aa0300bdf9646b8d9afb0048c", size = 1151126, upload-time = "2025-10-24T07:19:29.522Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b4/a0/89e6a024c3b4485b89ef86881c9d55e097e7cb0bdb74efb746f2fa6a9a76/lupa-2.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9abb98d5a8fd27c8285302e82199f0e56e463066f88f619d6594a450bf269d80", size = 2153693, upload-time = "2025-10-24T07:19:31.379Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b6/36/a0f007dc58fc1bbf51fb85dcc82fcb1f21b8c4261361de7dab0e3d8521ef/lupa-2.6-cp314-cp314t-win32.whl", hash = "sha256:1849efeba7a8f6fb8aa2c13790bee988fd242ae404bd459509640eeea3d1e291", size = 1590104, upload-time = "2025-10-24T07:19:33.514Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/7d/5e/db903ce9cf82c48d6b91bf6d63ae4c8d0d17958939a4e04ba6b9f38b8643/lupa-2.6-cp314-cp314t-win_amd64.whl", hash = "sha256:fc1498d1a4fc028bc521c26d0fad4ca00ed63b952e32fb95949bda76a04bad52", size = 1913818, upload-time = "2025-10-24T07:19:36.039Z" }, -] - [[package]] name = "mako" version = "1.3.10" @@ -1472,15 +979,6 @@ wheels = [ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/87/fb/99f81ac72ae23375f22b7afdb7642aba97c00a713c217124420147681a2f/mako-1.3.10-py3-none-any.whl", hash = "sha256:baef24a52fc4fc514a0887ac600f9f1cff3d82c61d4d700a1fa84d597b88db59", size = 78509, upload-time = "2025-04-10T12:50:53.297Z" }, ] -[[package]] -name = "markdown" -version = "3.10.2" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/2b/f4/69fa6ed85ae003c2378ffa8f6d2e3234662abd02c10d216c0ba96081a238/markdown-3.10.2.tar.gz", hash = "sha256:994d51325d25ad8aa7ce4ebaec003febcce822c3f8c911e3b17c52f7f589f950", size = 368805, upload-time = "2026-02-09T14:57:26.942Z" } -wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl", hash = "sha256:e91464b71ae3ee7afd3017d9f358ef0baf158fd9a298db92f1d4761133824c36", size = 108180, upload-time = "2026-02-09T14:57:25.787Z" }, -] - [[package]] name = "markdown-it-py" version = "4.0.0" @@ -1499,28 +997,6 @@ version = "3.0.3" source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e8/4b/3541d44f3937ba468b75da9eebcae497dcf67adb65caa16760b0a6807ebb/markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559", size = 11631, upload-time = "2025-09-27T18:36:05.558Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/98/1b/fbd8eed11021cabd9226c37342fa6ca4e8a98d8188a8d9b66740494960e4/markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419", size = 12057, upload-time = "2025-09-27T18:36:07.165Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/40/01/e560d658dc0bb8ab762670ece35281dec7b6c1b33f5fbc09ebb57a185519/markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695", size = 22050, upload-time = "2025-09-27T18:36:08.005Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/af/cd/ce6e848bbf2c32314c9b237839119c5a564a59725b53157c856e90937b7a/markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591", size = 20681, upload-time = "2025-09-27T18:36:08.881Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c9/2a/b5c12c809f1c3045c4d580b035a743d12fcde53cf685dbc44660826308da/markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c", size = 20705, upload-time = "2025-09-27T18:36:10.131Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/cf/e3/9427a68c82728d0a88c50f890d0fc072a1484de2f3ac1ad0bfc1a7214fd5/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f", size = 21524, upload-time = "2025-09-27T18:36:11.324Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/bc/36/23578f29e9e582a4d0278e009b38081dbe363c5e7165113fad546918a232/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6", size = 20282, upload-time = "2025-09-27T18:36:12.573Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/56/21/dca11354e756ebd03e036bd8ad58d6d7168c80ce1fe5e75218e4945cbab7/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1", size = 20745, upload-time = "2025-09-27T18:36:13.504Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/87/99/faba9369a7ad6e4d10b6a5fbf71fa2a188fe4a593b15f0963b73859a1bbd/markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa", size = 14571, upload-time = "2025-09-27T18:36:14.779Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d6/25/55dc3ab959917602c96985cb1253efaa4ff42f71194bddeb61eb7278b8be/markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8", size = 15056, upload-time = "2025-09-27T18:36:16.125Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d0/9e/0a02226640c255d1da0b8d12e24ac2aa6734da68bff14c05dd53b94a0fc3/markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1", size = 13932, upload-time = "2025-09-27T18:36:17.311Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, @@ -1587,93 +1063,12 @@ wheels = [ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, ] -[[package]] -name = "numpy" -version = "2.2.6" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } -resolution-markers = [ - "python_full_version < '3.11'", -] -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/76/21/7d2a95e4bba9dc13d043ee156a356c0a8f0c6309dff6b21b4d71a073b8a8/numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd", size = 20276440, upload-time = "2025-05-17T22:38:04.611Z" } -wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/9a/3e/ed6db5be21ce87955c0cbd3009f2803f59fa08df21b5df06862e2d8e2bdd/numpy-2.2.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b412caa66f72040e6d268491a59f2c43bf03eb6c96dd8f0307829feb7fa2b6fb", size = 21165245, upload-time = "2025-05-17T21:27:58.555Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/22/c2/4b9221495b2a132cc9d2eb862e21d42a009f5a60e45fc44b00118c174bff/numpy-2.2.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e41fd67c52b86603a91c1a505ebaef50b3314de0213461c7a6e99c9a3beff90", size = 14360048, upload-time = "2025-05-17T21:28:21.406Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/fd/77/dc2fcfc66943c6410e2bf598062f5959372735ffda175b39906d54f02349/numpy-2.2.6-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:37e990a01ae6ec7fe7fa1c26c55ecb672dd98b19c3d0e1d1f326fa13cb38d163", size = 5340542, upload-time = "2025-05-17T21:28:30.931Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/7a/4f/1cb5fdc353a5f5cc7feb692db9b8ec2c3d6405453f982435efc52561df58/numpy-2.2.6-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:5a6429d4be8ca66d889b7cf70f536a397dc45ba6faeb5f8c5427935d9592e9cf", size = 6878301, upload-time = "2025-05-17T21:28:41.613Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/eb/17/96a3acd228cec142fcb8723bd3cc39c2a474f7dcf0a5d16731980bcafa95/numpy-2.2.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efd28d4e9cd7d7a8d39074a4d44c63eda73401580c5c76acda2ce969e0a38e83", size = 14297320, upload-time = "2025-05-17T21:29:02.78Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b4/63/3de6a34ad7ad6646ac7d2f55ebc6ad439dbbf9c4370017c50cf403fb19b5/numpy-2.2.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc7b73d02efb0e18c000e9ad8b83480dfcd5dfd11065997ed4c6747470ae8915", size = 16801050, upload-time = "2025-05-17T21:29:27.675Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/07/b6/89d837eddef52b3d0cec5c6ba0456c1bf1b9ef6a6672fc2b7873c3ec4e2e/numpy-2.2.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:74d4531beb257d2c3f4b261bfb0fc09e0f9ebb8842d82a7b4209415896adc680", size = 15807034, upload-time = "2025-05-17T21:29:51.102Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/01/c8/dc6ae86e3c61cfec1f178e5c9f7858584049b6093f843bca541f94120920/numpy-2.2.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8fc377d995680230e83241d8a96def29f204b5782f371c532579b4f20607a289", size = 18614185, upload-time = "2025-05-17T21:30:18.703Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/5b/c5/0064b1b7e7c89137b471ccec1fd2282fceaae0ab3a9550f2568782d80357/numpy-2.2.6-cp310-cp310-win32.whl", hash = "sha256:b093dd74e50a8cba3e873868d9e93a85b78e0daf2e98c6797566ad8044e8363d", size = 6527149, upload-time = "2025-05-17T21:30:29.788Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a3/dd/4b822569d6b96c39d1215dbae0582fd99954dcbcf0c1a13c61783feaca3f/numpy-2.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:f0fd6321b839904e15c46e0d257fdd101dd7f530fe03fd6359c1ea63738703f3", size = 12904620, upload-time = "2025-05-17T21:30:48.994Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/da/a8/4f83e2aa666a9fbf56d6118faaaf5f1974d456b1823fda0a176eff722839/numpy-2.2.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f9f1adb22318e121c5c69a09142811a201ef17ab257a1e66ca3025065b7f53ae", size = 21176963, upload-time = "2025-05-17T21:31:19.36Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b3/2b/64e1affc7972decb74c9e29e5649fac940514910960ba25cd9af4488b66c/numpy-2.2.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c820a93b0255bc360f53eca31a0e676fd1101f673dda8da93454a12e23fc5f7a", size = 14406743, upload-time = "2025-05-17T21:31:41.087Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/4a/9f/0121e375000b5e50ffdd8b25bf78d8e1a5aa4cca3f185d41265198c7b834/numpy-2.2.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3d70692235e759f260c3d837193090014aebdf026dfd167834bcba43e30c2a42", size = 5352616, upload-time = "2025-05-17T21:31:50.072Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/31/0d/b48c405c91693635fbe2dcd7bc84a33a602add5f63286e024d3b6741411c/numpy-2.2.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:481b49095335f8eed42e39e8041327c05b0f6f4780488f61286ed3c01368d491", size = 6889579, upload-time = "2025-05-17T21:32:01.712Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/52/b8/7f0554d49b565d0171eab6e99001846882000883998e7b7d9f0d98b1f934/numpy-2.2.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b64d8d4d17135e00c8e346e0a738deb17e754230d7e0810ac5012750bbd85a5a", size = 14312005, upload-time = "2025-05-17T21:32:23.332Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b3/dd/2238b898e51bd6d389b7389ffb20d7f4c10066d80351187ec8e303a5a475/numpy-2.2.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba10f8411898fc418a521833e014a77d3ca01c15b0c6cdcce6a0d2897e6dbbdf", size = 16821570, upload-time = "2025-05-17T21:32:47.991Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/83/6c/44d0325722cf644f191042bf47eedad61c1e6df2432ed65cbe28509d404e/numpy-2.2.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bd48227a919f1bafbdda0583705e547892342c26fb127219d60a5c36882609d1", size = 15818548, upload-time = "2025-05-17T21:33:11.728Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ae/9d/81e8216030ce66be25279098789b665d49ff19eef08bfa8cb96d4957f422/numpy-2.2.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9551a499bf125c1d4f9e250377c1ee2eddd02e01eac6644c080162c0c51778ab", size = 18620521, upload-time = "2025-05-17T21:33:39.139Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/6a/fd/e19617b9530b031db51b0926eed5345ce8ddc669bb3bc0044b23e275ebe8/numpy-2.2.6-cp311-cp311-win32.whl", hash = "sha256:0678000bb9ac1475cd454c6b8c799206af8107e310843532b04d49649c717a47", size = 6525866, upload-time = "2025-05-17T21:33:50.273Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/31/0a/f354fb7176b81747d870f7991dc763e157a934c717b67b58456bc63da3df/numpy-2.2.6-cp311-cp311-win_amd64.whl", hash = "sha256:e8213002e427c69c45a52bbd94163084025f533a55a59d6f9c5b820774ef3303", size = 12907455, upload-time = "2025-05-17T21:34:09.135Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/82/5d/c00588b6cf18e1da539b45d3598d3557084990dcc4331960c15ee776ee41/numpy-2.2.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff", size = 20875348, upload-time = "2025-05-17T21:34:39.648Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/66/ee/560deadcdde6c2f90200450d5938f63a34b37e27ebff162810f716f6a230/numpy-2.2.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c", size = 14119362, upload-time = "2025-05-17T21:35:01.241Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/3c/65/4baa99f1c53b30adf0acd9a5519078871ddde8d2339dc5a7fde80d9d87da/numpy-2.2.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3", size = 5084103, upload-time = "2025-05-17T21:35:10.622Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/cc/89/e5a34c071a0570cc40c9a54eb472d113eea6d002e9ae12bb3a8407fb912e/numpy-2.2.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:71594f7c51a18e728451bb50cc60a3ce4e6538822731b2933209a1f3614e9282", size = 6625382, upload-time = "2025-05-17T21:35:21.414Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f8/35/8c80729f1ff76b3921d5c9487c7ac3de9b2a103b1cd05e905b3090513510/numpy-2.2.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2618db89be1b4e05f7a1a847a9c1c0abd63e63a1607d892dd54668dd92faf87", size = 14018462, upload-time = "2025-05-17T21:35:42.174Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/8c/3d/1e1db36cfd41f895d266b103df00ca5b3cbe965184df824dec5c08c6b803/numpy-2.2.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249", size = 16527618, upload-time = "2025-05-17T21:36:06.711Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/61/c6/03ed30992602c85aa3cd95b9070a514f8b3c33e31124694438d88809ae36/numpy-2.2.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37c0ca431f82cd5fa716eca9506aefcabc247fb27ba69c5062a6d3ade8cf8f49", size = 15505511, upload-time = "2025-05-17T21:36:29.965Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b7/25/5761d832a81df431e260719ec45de696414266613c9ee268394dd5ad8236/numpy-2.2.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de", size = 18313783, upload-time = "2025-05-17T21:36:56.883Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/57/0a/72d5a3527c5ebffcd47bde9162c39fae1f90138c961e5296491ce778e682/numpy-2.2.6-cp312-cp312-win32.whl", hash = "sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4", size = 6246506, upload-time = "2025-05-17T21:37:07.368Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/36/fa/8c9210162ca1b88529ab76b41ba02d433fd54fecaf6feb70ef9f124683f1/numpy-2.2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2", size = 12614190, upload-time = "2025-05-17T21:37:26.213Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f9/5c/6657823f4f594f72b5471f1db1ab12e26e890bb2e41897522d134d2a3e81/numpy-2.2.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0811bb762109d9708cca4d0b13c4f67146e3c3b7cf8d34018c722adb2d957c84", size = 20867828, upload-time = "2025-05-17T21:37:56.699Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/dc/9e/14520dc3dadf3c803473bd07e9b2bd1b69bc583cb2497b47000fed2fa92f/numpy-2.2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:287cc3162b6f01463ccd86be154f284d0893d2b3ed7292439ea97eafa8170e0b", size = 14143006, upload-time = "2025-05-17T21:38:18.291Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/4f/06/7e96c57d90bebdce9918412087fc22ca9851cceaf5567a45c1f404480e9e/numpy-2.2.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f1372f041402e37e5e633e586f62aa53de2eac8d98cbfb822806ce4bbefcb74d", size = 5076765, upload-time = "2025-05-17T21:38:27.319Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/73/ed/63d920c23b4289fdac96ddbdd6132e9427790977d5457cd132f18e76eae0/numpy-2.2.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:55a4d33fa519660d69614a9fad433be87e5252f4b03850642f88993f7b2ca566", size = 6617736, upload-time = "2025-05-17T21:38:38.141Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/85/c5/e19c8f99d83fd377ec8c7e0cf627a8049746da54afc24ef0a0cb73d5dfb5/numpy-2.2.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f92729c95468a2f4f15e9bb94c432a9229d0d50de67304399627a943201baa2f", size = 14010719, upload-time = "2025-05-17T21:38:58.433Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/19/49/4df9123aafa7b539317bf6d342cb6d227e49f7a35b99c287a6109b13dd93/numpy-2.2.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bc23a79bfabc5d056d106f9befb8d50c31ced2fbc70eedb8155aec74a45798f", size = 16526072, upload-time = "2025-05-17T21:39:22.638Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b2/6c/04b5f47f4f32f7c2b0e7260442a8cbcf8168b0e1a41ff1495da42f42a14f/numpy-2.2.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e3143e4451880bed956e706a3220b4e5cf6172ef05fcc397f6f36a550b1dd868", size = 15503213, upload-time = "2025-05-17T21:39:45.865Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/17/0a/5cd92e352c1307640d5b6fec1b2ffb06cd0dabe7d7b8227f97933d378422/numpy-2.2.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4f13750ce79751586ae2eb824ba7e1e8dba64784086c98cdbbcc6a42112ce0d", size = 18316632, upload-time = "2025-05-17T21:40:13.331Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f0/3b/5cba2b1d88760ef86596ad0f3d484b1cbff7c115ae2429678465057c5155/numpy-2.2.6-cp313-cp313-win32.whl", hash = "sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd", size = 6244532, upload-time = "2025-05-17T21:43:46.099Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/cb/3b/d58c12eafcb298d4e6d0d40216866ab15f59e55d148a5658bb3132311fcf/numpy-2.2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c", size = 12610885, upload-time = "2025-05-17T21:44:05.145Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/6b/9e/4bf918b818e516322db999ac25d00c75788ddfd2d2ade4fa66f1f38097e1/numpy-2.2.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0bca768cd85ae743b2affdc762d617eddf3bcf8724435498a1e80132d04879e6", size = 20963467, upload-time = "2025-05-17T21:40:44Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/61/66/d2de6b291507517ff2e438e13ff7b1e2cdbdb7cb40b3ed475377aece69f9/numpy-2.2.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fc0c5673685c508a142ca65209b4e79ed6740a4ed6b2267dbba90f34b0b3cfda", size = 14225144, upload-time = "2025-05-17T21:41:05.695Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e4/25/480387655407ead912e28ba3a820bc69af9adf13bcbe40b299d454ec011f/numpy-2.2.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:5bd4fc3ac8926b3819797a7c0e2631eb889b4118a9898c84f585a54d475b7e40", size = 5200217, upload-time = "2025-05-17T21:41:15.903Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/aa/4a/6e313b5108f53dcbf3aca0c0f3e9c92f4c10ce57a0a721851f9785872895/numpy-2.2.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8", size = 6712014, upload-time = "2025-05-17T21:41:27.321Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b7/30/172c2d5c4be71fdf476e9de553443cf8e25feddbe185e0bd88b096915bcc/numpy-2.2.6-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1dda9c7e08dc141e0247a5b8f49cf05984955246a327d4c48bda16821947b2f", size = 14077935, upload-time = "2025-05-17T21:41:49.738Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/12/fb/9e743f8d4e4d3c710902cf87af3512082ae3d43b945d5d16563f26ec251d/numpy-2.2.6-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f447e6acb680fd307f40d3da4852208af94afdfab89cf850986c3ca00562f4fa", size = 16600122, upload-time = "2025-05-17T21:42:14.046Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/12/75/ee20da0e58d3a66f204f38916757e01e33a9737d0b22373b3eb5a27358f9/numpy-2.2.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:389d771b1623ec92636b0786bc4ae56abafad4a4c513d36a55dce14bd9ce8571", size = 15586143, upload-time = "2025-05-17T21:42:37.464Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/76/95/bef5b37f29fc5e739947e9ce5179ad402875633308504a52d188302319c8/numpy-2.2.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8e9ace4a37db23421249ed236fdcdd457d671e25146786dfc96835cd951aa7c1", size = 18385260, upload-time = "2025-05-17T21:43:05.189Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/09/04/f2f83279d287407cf36a7a8053a5abe7be3622a4363337338f2585e4afda/numpy-2.2.6-cp313-cp313t-win32.whl", hash = "sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff", size = 6377225, upload-time = "2025-05-17T21:43:16.254Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/67/0e/35082d13c09c02c011cf21570543d202ad929d961c02a147493cb0c2bdf5/numpy-2.2.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06", size = 12771374, upload-time = "2025-05-17T21:43:35.479Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/9e/3b/d94a75f4dbf1ef5d321523ecac21ef23a3cd2ac8b78ae2aac40873590229/numpy-2.2.6-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0b605b275d7bd0c640cad4e5d30fa701a8d59302e127e5f79138ad62762c3e3d", size = 21040391, upload-time = "2025-05-17T21:44:35.948Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/17/f4/09b2fa1b58f0fb4f7c7963a1649c64c4d315752240377ed74d9cd878f7b5/numpy-2.2.6-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:7befc596a7dc9da8a337f79802ee8adb30a552a94f792b9c9d18c840055907db", size = 6786754, upload-time = "2025-05-17T21:44:47.446Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/af/30/feba75f143bdc868a1cc3f44ccfa6c4b9ec522b36458e738cd00f67b573f/numpy-2.2.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce47521a4754c8f4593837384bd3424880629f718d87c5d44f8ed763edd63543", size = 16643476, upload-time = "2025-05-17T21:45:11.871Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/37/48/ac2a9584402fb6c0cd5b5d1a91dcf176b15760130dd386bbafdbfe3640bf/numpy-2.2.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d042d24c90c41b54fd506da306759e06e568864df8ec17ccc17e9e884634fd00", size = 12812666, upload-time = "2025-05-17T21:45:31.426Z" }, -] - [[package]] name = "numpy" version = "2.4.1" source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } -resolution-markers = [ - "python_full_version >= '3.13'", - "python_full_version == '3.12.*'", - "python_full_version == '3.11.*'", -] sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/24/62/ae72ff66c0f1fd959925b4c11f8c2dea61f47f6acaea75a08512cdfe3fed/numpy-2.4.1.tar.gz", hash = "sha256:a1ceafc5042451a858231588a104093474c6a5c57dcc724841f5c888d237d690", size = 20721320, upload-time = "2026-01-10T06:44:59.619Z" } wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a5/34/2b1bc18424f3ad9af577f6ce23600319968a70575bd7db31ce66731bbef9/numpy-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0cce2a669e3c8ba02ee563c7835f92c153cf02edff1ae05e1823f1dde21b16a5", size = 16944563, upload-time = "2026-01-10T06:42:14.615Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/2c/57/26e5f97d075aef3794045a6ca9eada6a4ed70eb9a40e7a4a93f9ac80d704/numpy-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:899d2c18024984814ac7e83f8f49d8e8180e2fbe1b2e252f2e7f1d06bea92425", size = 12645658, upload-time = "2026-01-10T06:42:17.298Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/8e/ba/80fc0b1e3cb2fd5c6143f00f42eb67762aa043eaa05ca924ecc3222a7849/numpy-2.4.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:09aa8a87e45b55a1c2c205d42e2808849ece5c484b2aab11fecabec3841cafba", size = 5474132, upload-time = "2026-01-10T06:42:19.637Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/40/ae/0a5b9a397f0e865ec171187c78d9b57e5588afc439a04ba9cab1ebb2c945/numpy-2.4.1-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:edee228f76ee2dab4579fad6f51f6a305de09d444280109e0f75df247ff21501", size = 6804159, upload-time = "2026-01-10T06:42:21.44Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/86/9c/841c15e691c7085caa6fd162f063eff494099c8327aeccd509d1ab1e36ab/numpy-2.4.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a92f227dbcdc9e4c3e193add1a189a9909947d4f8504c576f4a732fd0b54240a", size = 14708058, upload-time = "2026-01-10T06:42:23.546Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/5d/9d/7862db06743f489e6a502a3b93136d73aea27d97b2cf91504f70a27501d6/numpy-2.4.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:538bf4ec353709c765ff75ae616c34d3c3dca1a68312727e8f2676ea644f8509", size = 16651501, upload-time = "2026-01-10T06:42:25.909Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a6/9c/6fc34ebcbd4015c6e5f0c0ce38264010ce8a546cb6beacb457b84a75dfc8/numpy-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ac08c63cb7779b85e9d5318e6c3518b424bc1f364ac4cb2c6136f12e5ff2dccc", size = 16492627, upload-time = "2026-01-10T06:42:28.938Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/aa/63/2494a8597502dacda439f61b3c0db4da59928150e62be0e99395c3ad23c5/numpy-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4f9c360ecef085e5841c539a9a12b883dff005fbd7ce46722f5e9cef52634d82", size = 18585052, upload-time = "2026-01-10T06:42:31.312Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/6a/93/098e1162ae7522fc9b618d6272b77404c4656c72432ecee3abc029aa3de0/numpy-2.4.1-cp311-cp311-win32.whl", hash = "sha256:0f118ce6b972080ba0758c6087c3617b5ba243d806268623dc34216d69099ba0", size = 6236575, upload-time = "2026-01-10T06:42:33.872Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/8c/de/f5e79650d23d9e12f38a7bc6b03ea0835b9575494f8ec94c11c6e773b1b1/numpy-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:18e14c4d09d55eef39a6ab5b08406e84bc6869c1e34eef45564804f90b7e0574", size = 12604479, upload-time = "2026-01-10T06:42:35.778Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/dd/65/e1097a7047cff12ce3369bd003811516b20ba1078dbdec135e1cd7c16c56/numpy-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:6461de5113088b399d655d45c3897fa188766415d0f568f175ab071c8873bd73", size = 10578325, upload-time = "2026-01-10T06:42:38.518Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/78/7f/ec53e32bf10c813604edf07a3682616bd931d026fcde7b6d13195dfb684a/numpy-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d3703409aac693fa82c0aee023a1ae06a6e9d065dba10f5e8e80f642f1e9d0a2", size = 16656888, upload-time = "2026-01-10T06:42:40.913Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b8/e0/1f9585d7dae8f14864e948fd7fa86c6cb72dee2676ca2748e63b1c5acfe0/numpy-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7211b95ca365519d3596a1d8688a95874cc94219d417504d9ecb2df99fa7bfa8", size = 12373956, upload-time = "2026-01-10T06:42:43.091Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/8e/43/9762e88909ff2326f5e7536fa8cb3c49fb03a7d92705f23e6e7f553d9cb3/numpy-2.4.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:5adf01965456a664fc727ed69cc71848f28d063217c63e1a0e200a118d5eec9a", size = 5202567, upload-time = "2026-01-10T06:42:45.107Z" }, @@ -1727,22 +1122,6 @@ wheels = [ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/23/12/8b5fc6b9c487a09a7957188e0943c9ff08432c65e34567cabc1623b03a51/numpy-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:5de60946f14ebe15e713a6f22850c2372fa72f4ff9a432ab44aa90edcadaa65a", size = 6152482, upload-time = "2026-01-10T06:44:36.798Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/00/a5/9f8ca5856b8940492fc24fbe13c1bc34d65ddf4079097cf9e53164d094e1/numpy-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:8f085da926c0d491ffff3096f91078cc97ea67e7e6b65e490bc8dcda65663be2", size = 12627117, upload-time = "2026-01-10T06:44:38.828Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ad/0d/eca3d962f9eef265f01a8e0d20085c6dd1f443cbffc11b6dede81fd82356/numpy-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:6436cffb4f2bf26c974344439439c95e152c9a527013f26b3577be6c2ca64295", size = 10667121, upload-time = "2026-01-10T06:44:41.644Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/1e/48/d86f97919e79314a1cdee4c832178763e6e98e623e123d0bada19e92c15a/numpy-2.4.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:8ad35f20be147a204e28b6a0575fbf3540c5e5f802634d4258d55b1ff5facce1", size = 16822202, upload-time = "2026-01-10T06:44:43.738Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/51/e9/1e62a7f77e0f37dcfb0ad6a9744e65df00242b6ea37dfafb55debcbf5b55/numpy-2.4.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:8097529164c0f3e32bb89412a0905d9100bf434d9692d9fc275e18dcf53c9344", size = 12569985, upload-time = "2026-01-10T06:44:45.945Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c7/7e/914d54f0c801342306fdcdce3e994a56476f1b818c46c47fc21ae968088c/numpy-2.4.1-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:ea66d2b41ca4a1630aae5507ee0a71647d3124d1741980138aa8f28f44dac36e", size = 5398484, upload-time = "2026-01-10T06:44:48.012Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/1c/d8/9570b68584e293a33474e7b5a77ca404f1dcc655e40050a600dee81d27fb/numpy-2.4.1-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:d3f8f0df9f4b8be57b3bf74a1d087fec68f927a2fab68231fdb442bf2c12e426", size = 6713216, upload-time = "2026-01-10T06:44:49.725Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/33/9b/9dd6e2db8d49eb24f86acaaa5258e5f4c8ed38209a4ee9de2d1a0ca25045/numpy-2.4.1-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2023ef86243690c2791fd6353e5b4848eedaa88ca8a2d129f462049f6d484696", size = 14538937, upload-time = "2026-01-10T06:44:51.498Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/53/87/d5bd995b0f798a37105b876350d346eea5838bd8f77ea3d7a48392f3812b/numpy-2.4.1-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8361ea4220d763e54cff2fbe7d8c93526b744f7cd9ddab47afeff7e14e8503be", size = 16479830, upload-time = "2026-01-10T06:44:53.931Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/5b/c7/b801bf98514b6ae6475e941ac05c58e6411dd863ea92916bfd6d510b08c1/numpy-2.4.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:4f1b68ff47680c2925f8063402a693ede215f0257f02596b1318ecdfb1d79e33", size = 12492579, upload-time = "2026-01-10T06:44:57.094Z" }, -] - -[[package]] -name = "oauthlib" -version = "3.3.1" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/0b/5f/19930f824ffeb0ad4372da4812c50edbd1434f678c90c2733e1188edfc63/oauthlib-3.3.1.tar.gz", hash = "sha256:0f0f8aa759826a193cf66c12ea1af1637f87b9b4622d46e866952bb022e538c9", size = 185918, upload-time = "2025-06-19T22:48:08.269Z" } -wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/be/9c/92789c596b8df838baa98fa71844d84283302f7604ed565dafe5a6b5041a/oauthlib-3.3.1-py3-none-any.whl", hash = "sha256:88119c938d2b8fb88561af5f6ee0eec8cc8d552b7bb1f712743136eb7523b7a1", size = 160065, upload-time = "2025-06-19T22:48:06.508Z" }, ] [[package]] @@ -1776,100 +1155,6 @@ wheels = [ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c0/da/977ded879c29cbd04de313843e76868e6e13408a94ed6b987245dc7c8506/openpyxl-3.1.5-py2.py3-none-any.whl", hash = "sha256:5282c12b107bffeef825f4617dc029afaf41d0ea60823bbb665ef3079dc79de2", size = 250910, upload-time = "2024-06-28T14:03:41.161Z" }, ] -[[package]] -name = "opentelemetry-api" -version = "1.39.1" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } -dependencies = [ - { name = "importlib-metadata" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/97/b9/3161be15bb8e3ad01be8be5a968a9237c3027c5be504362ff800fca3e442/opentelemetry_api-1.39.1.tar.gz", hash = "sha256:fbde8c80e1b937a2c61f20347e91c0c18a1940cecf012d62e65a7caf08967c9c", size = 65767, upload-time = "2025-12-11T13:32:39.182Z" } -wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/cf/df/d3f1ddf4bb4cb50ed9b1139cc7b1c54c34a1e7ce8fd1b9a37c0d1551a6bd/opentelemetry_api-1.39.1-py3-none-any.whl", hash = "sha256:2edd8463432a7f8443edce90972169b195e7d6a05500cd29e6d13898187c9950", size = 66356, upload-time = "2025-12-11T13:32:17.304Z" }, -] - -[[package]] -name = "orjson" -version = "3.11.5" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/04/b8/333fdb27840f3bf04022d21b654a35f58e15407183aeb16f3b41aa053446/orjson-3.11.5.tar.gz", hash = "sha256:82393ab47b4fe44ffd0a7659fa9cfaacc717eb617c93cde83795f14af5c2e9d5", size = 5972347, upload-time = "2025-12-06T15:55:39.458Z" } -wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/79/19/b22cf9dad4db20c8737041046054cbd4f38bb5a2d0e4bb60487832ce3d76/orjson-3.11.5-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:df9eadb2a6386d5ea2bfd81309c505e125cfc9ba2b1b99a97e60985b0b3665d1", size = 245719, upload-time = "2025-12-06T15:53:43.877Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/03/2e/b136dd6bf30ef5143fbe76a4c142828b55ccc618be490201e9073ad954a1/orjson-3.11.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ccc70da619744467d8f1f49a8cadae5ec7bbe054e5232d95f92ed8737f8c5870", size = 132467, upload-time = "2025-12-06T15:53:45.379Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ae/fc/ae99bfc1e1887d20a0268f0e2686eb5b13d0ea7bbe01de2b566febcd2130/orjson-3.11.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:073aab025294c2f6fc0807201c76fdaed86f8fc4be52c440fb78fbb759a1ac09", size = 130702, upload-time = "2025-12-06T15:53:46.659Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/6e/43/ef7912144097765997170aca59249725c3ab8ef6079f93f9d708dd058df5/orjson-3.11.5-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:835f26fa24ba0bb8c53ae2a9328d1706135b74ec653ed933869b74b6909e63fd", size = 135907, upload-time = "2025-12-06T15:53:48.487Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/3f/da/24d50e2d7f4092ddd4d784e37a3fa41f22ce8ed97abc9edd222901a96e74/orjson-3.11.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:667c132f1f3651c14522a119e4dd631fad98761fa960c55e8e7430bb2a1ba4ac", size = 139935, upload-time = "2025-12-06T15:53:49.88Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/02/4a/b4cb6fcbfff5b95a3a019a8648255a0fac9b221fbf6b6e72be8df2361feb/orjson-3.11.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:42e8961196af655bb5e63ce6c60d25e8798cd4dfbc04f4203457fa3869322c2e", size = 137541, upload-time = "2025-12-06T15:53:51.226Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a5/99/a11bd129f18c2377c27b2846a9d9be04acec981f770d711ba0aaea563984/orjson-3.11.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75412ca06e20904c19170f8a24486c4e6c7887dea591ba18a1ab572f1300ee9f", size = 139031, upload-time = "2025-12-06T15:53:52.309Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/64/29/d7b77d7911574733a036bb3e8ad7053ceb2b7d6ea42208b9dbc55b23b9ed/orjson-3.11.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6af8680328c69e15324b5af3ae38abbfcf9cbec37b5346ebfd52339c3d7e8a18", size = 141622, upload-time = "2025-12-06T15:53:53.606Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/93/41/332db96c1de76b2feda4f453e91c27202cd092835936ce2b70828212f726/orjson-3.11.5-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:a86fe4ff4ea523eac8f4b57fdac319faf037d3c1be12405e6a7e86b3fbc4756a", size = 413800, upload-time = "2025-12-06T15:53:54.866Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/76/e1/5a0d148dd1f89ad2f9651df67835b209ab7fcb1118658cf353425d7563e9/orjson-3.11.5-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e607b49b1a106ee2086633167033afbd63f76f2999e9236f638b06b112b24ea7", size = 151198, upload-time = "2025-12-06T15:53:56.383Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/0d/96/8db67430d317a01ae5cf7971914f6775affdcfe99f5bff9ef3da32492ecc/orjson-3.11.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7339f41c244d0eea251637727f016b3d20050636695bc78345cce9029b189401", size = 141984, upload-time = "2025-12-06T15:53:57.746Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/71/49/40d21e1aa1ac569e521069228bb29c9b5a350344ccf922a0227d93c2ed44/orjson-3.11.5-cp310-cp310-win32.whl", hash = "sha256:8be318da8413cdbbce77b8c5fac8d13f6eb0f0db41b30bb598631412619572e8", size = 135272, upload-time = "2025-12-06T15:53:59.769Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c4/7e/d0e31e78be0c100e08be64f48d2850b23bcb4d4c70d114f4e43b39f6895a/orjson-3.11.5-cp310-cp310-win_amd64.whl", hash = "sha256:b9f86d69ae822cabc2a0f6c099b43e8733dda788405cba2665595b7e8dd8d167", size = 133360, upload-time = "2025-12-06T15:54:01.25Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/fd/68/6b3659daec3a81aed5ab47700adb1a577c76a5452d35b91c88efee89987f/orjson-3.11.5-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9c8494625ad60a923af6b2b0bd74107146efe9b55099e20d7740d995f338fcd8", size = 245318, upload-time = "2025-12-06T15:54:02.355Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e9/00/92db122261425f61803ccf0830699ea5567439d966cbc35856fe711bfe6b/orjson-3.11.5-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:7bb2ce0b82bc9fd1168a513ddae7a857994b780b2945a8c51db4ab1c4b751ebc", size = 129491, upload-time = "2025-12-06T15:54:03.877Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/94/4f/ffdcb18356518809d944e1e1f77589845c278a1ebbb5a8297dfefcc4b4cb/orjson-3.11.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67394d3becd50b954c4ecd24ac90b5051ee7c903d167459f93e77fc6f5b4c968", size = 132167, upload-time = "2025-12-06T15:54:04.944Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/97/c6/0a8caff96f4503f4f7dd44e40e90f4d14acf80d3b7a97cb88747bb712d3e/orjson-3.11.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:298d2451f375e5f17b897794bcc3e7b821c0f32b4788b9bcae47ada24d7f3cf7", size = 130516, upload-time = "2025-12-06T15:54:06.274Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/4d/63/43d4dc9bd9954bff7052f700fdb501067f6fb134a003ddcea2a0bb3854ed/orjson-3.11.5-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa5e4244063db8e1d87e0f54c3f7522f14b2dc937e65d5241ef0076a096409fd", size = 135695, upload-time = "2025-12-06T15:54:07.702Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/87/6f/27e2e76d110919cb7fcb72b26166ee676480a701bcf8fc53ac5d0edce32f/orjson-3.11.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1db2088b490761976c1b2e956d5d4e6409f3732e9d79cfa69f876c5248d1baf9", size = 139664, upload-time = "2025-12-06T15:54:08.828Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d4/f8/5966153a5f1be49b5fbb8ca619a529fde7bc71aa0a376f2bb83fed248bcd/orjson-3.11.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c2ed66358f32c24e10ceea518e16eb3549e34f33a9d51f99ce23b0251776a1ef", size = 137289, upload-time = "2025-12-06T15:54:09.898Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a7/34/8acb12ff0299385c8bbcbb19fbe40030f23f15a6de57a9c587ebf71483fb/orjson-3.11.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c2021afda46c1ed64d74b555065dbd4c2558d510d8cec5ea6a53001b3e5e82a9", size = 138784, upload-time = "2025-12-06T15:54:11.022Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ee/27/910421ea6e34a527f73d8f4ee7bdffa48357ff79c7b8d6eb6f7b82dd1176/orjson-3.11.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b42ffbed9128e547a1647a3e50bc88ab28ae9daa61713962e0d3dd35e820c125", size = 141322, upload-time = "2025-12-06T15:54:12.427Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/87/a3/4b703edd1a05555d4bb1753d6ce44e1a05b7a6d7c164d5b332c795c63d70/orjson-3.11.5-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:8d5f16195bb671a5dd3d1dbea758918bada8f6cc27de72bd64adfbd748770814", size = 413612, upload-time = "2025-12-06T15:54:13.858Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/1b/36/034177f11d7eeea16d3d2c42a1883b0373978e08bc9dad387f5074c786d8/orjson-3.11.5-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c0e5d9f7a0227df2927d343a6e3859bebf9208b427c79bd31949abcc2fa32fa5", size = 150993, upload-time = "2025-12-06T15:54:15.189Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/44/2f/ea8b24ee046a50a7d141c0227c4496b1180b215e728e3b640684f0ea448d/orjson-3.11.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:23d04c4543e78f724c4dfe656b3791b5f98e4c9253e13b2636f1af5d90e4a880", size = 141774, upload-time = "2025-12-06T15:54:16.451Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/8a/12/cc440554bf8200eb23348a5744a575a342497b65261cd65ef3b28332510a/orjson-3.11.5-cp311-cp311-win32.whl", hash = "sha256:c404603df4865f8e0afe981aa3c4b62b406e6d06049564d58934860b62b7f91d", size = 135109, upload-time = "2025-12-06T15:54:17.73Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a3/83/e0c5aa06ba73a6760134b169f11fb970caa1525fa4461f94d76e692299d9/orjson-3.11.5-cp311-cp311-win_amd64.whl", hash = "sha256:9645ef655735a74da4990c24ffbd6894828fbfa117bc97c1edd98c282ecb52e1", size = 133193, upload-time = "2025-12-06T15:54:19.426Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/cb/35/5b77eaebc60d735e832c5b1a20b155667645d123f09d471db0a78280fb49/orjson-3.11.5-cp311-cp311-win_arm64.whl", hash = "sha256:1cbf2735722623fcdee8e712cbaaab9e372bbcb0c7924ad711b261c2eccf4a5c", size = 126830, upload-time = "2025-12-06T15:54:20.836Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ef/a4/8052a029029b096a78955eadd68ab594ce2197e24ec50e6b6d2ab3f4e33b/orjson-3.11.5-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:334e5b4bff9ad101237c2d799d9fd45737752929753bf4faf4b207335a416b7d", size = 245347, upload-time = "2025-12-06T15:54:22.061Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/64/67/574a7732bd9d9d79ac620c8790b4cfe0717a3d5a6eb2b539e6e8995e24a0/orjson-3.11.5-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:ff770589960a86eae279f5d8aa536196ebda8273a2a07db2a54e82b93bc86626", size = 129435, upload-time = "2025-12-06T15:54:23.615Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/52/8d/544e77d7a29d90cf4d9eecd0ae801c688e7f3d1adfa2ebae5e1e94d38ab9/orjson-3.11.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed24250e55efbcb0b35bed7caaec8cedf858ab2f9f2201f17b8938c618c8ca6f", size = 132074, upload-time = "2025-12-06T15:54:24.694Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/6e/57/b9f5b5b6fbff9c26f77e785baf56ae8460ef74acdb3eae4931c25b8f5ba9/orjson-3.11.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a66d7769e98a08a12a139049aac2f0ca3adae989817f8c43337455fbc7669b85", size = 130520, upload-time = "2025-12-06T15:54:26.185Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f6/6d/d34970bf9eb33f9ec7c979a262cad86076814859e54eb9a059a52f6dc13d/orjson-3.11.5-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:86cfc555bfd5794d24c6a1903e558b50644e5e68e6471d66502ce5cb5fdef3f9", size = 136209, upload-time = "2025-12-06T15:54:27.264Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e7/39/bc373b63cc0e117a105ea12e57280f83ae52fdee426890d57412432d63b3/orjson-3.11.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a230065027bc2a025e944f9d4714976a81e7ecfa940923283bca7bbc1f10f626", size = 139837, upload-time = "2025-12-06T15:54:28.75Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/cb/aa/7c4818c8d7d324da220f4f1af55c343956003aa4d1ce1857bdc1d396ba69/orjson-3.11.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b29d36b60e606df01959c4b982729c8845c69d1963f88686608be9ced96dbfaa", size = 137307, upload-time = "2025-12-06T15:54:29.856Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/46/bf/0993b5a056759ba65145effe3a79dd5a939d4a070eaa5da2ee3180fbb13f/orjson-3.11.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c74099c6b230d4261fdc3169d50efc09abf38ace1a42ea2f9994b1d79153d477", size = 139020, upload-time = "2025-12-06T15:54:31.024Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/65/e8/83a6c95db3039e504eda60fc388f9faedbb4f6472f5aba7084e06552d9aa/orjson-3.11.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e697d06ad57dd0c7a737771d470eedc18e68dfdefcdd3b7de7f33dfda5b6212e", size = 141099, upload-time = "2025-12-06T15:54:32.196Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b9/b4/24fdc024abfce31c2f6812973b0a693688037ece5dc64b7a60c1ce69e2f2/orjson-3.11.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:e08ca8a6c851e95aaecc32bc44a5aa75d0ad26af8cdac7c77e4ed93acf3d5b69", size = 413540, upload-time = "2025-12-06T15:54:33.361Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d9/37/01c0ec95d55ed0c11e4cae3e10427e479bba40c77312b63e1f9665e0737d/orjson-3.11.5-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e8b5f96c05fce7d0218df3fdfeb962d6b8cfff7e3e20264306b46dd8b217c0f3", size = 151530, upload-time = "2025-12-06T15:54:34.6Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f9/d4/f9ebc57182705bb4bbe63f5bbe14af43722a2533135e1d2fb7affa0c355d/orjson-3.11.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ddbfdb5099b3e6ba6d6ea818f61997bb66de14b411357d24c4612cf1ebad08ca", size = 141863, upload-time = "2025-12-06T15:54:35.801Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/0d/04/02102b8d19fdcb009d72d622bb5781e8f3fae1646bf3e18c53d1bc8115b5/orjson-3.11.5-cp312-cp312-win32.whl", hash = "sha256:9172578c4eb09dbfcf1657d43198de59b6cef4054de385365060ed50c458ac98", size = 135255, upload-time = "2025-12-06T15:54:37.209Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d4/fb/f05646c43d5450492cb387de5549f6de90a71001682c17882d9f66476af5/orjson-3.11.5-cp312-cp312-win_amd64.whl", hash = "sha256:2b91126e7b470ff2e75746f6f6ee32b9ab67b7a93c8ba1d15d3a0caaf16ec875", size = 133252, upload-time = "2025-12-06T15:54:38.401Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/dc/a6/7b8c0b26ba18c793533ac1cd145e131e46fcf43952aa94c109b5b913c1f0/orjson-3.11.5-cp312-cp312-win_arm64.whl", hash = "sha256:acbc5fac7e06777555b0722b8ad5f574739e99ffe99467ed63da98f97f9ca0fe", size = 126777, upload-time = "2025-12-06T15:54:39.515Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/10/43/61a77040ce59f1569edf38f0b9faadc90c8cf7e9bec2e0df51d0132c6bb7/orjson-3.11.5-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:3b01799262081a4c47c035dd77c1301d40f568f77cc7ec1bb7db5d63b0a01629", size = 245271, upload-time = "2025-12-06T15:54:40.878Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/55/f9/0f79be617388227866d50edd2fd320cb8fb94dc1501184bb1620981a0aba/orjson-3.11.5-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:61de247948108484779f57a9f406e4c84d636fa5a59e411e6352484985e8a7c3", size = 129422, upload-time = "2025-12-06T15:54:42.403Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/77/42/f1bf1549b432d4a78bfa95735b79b5dac75b65b5bb815bba86ad406ead0a/orjson-3.11.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:894aea2e63d4f24a7f04a1908307c738d0dce992e9249e744b8f4e8dd9197f39", size = 132060, upload-time = "2025-12-06T15:54:43.531Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/25/49/825aa6b929f1a6ed244c78acd7b22c1481fd7e5fda047dc8bf4c1a807eb6/orjson-3.11.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ddc21521598dbe369d83d4d40338e23d4101dad21dae0e79fa20465dbace019f", size = 130391, upload-time = "2025-12-06T15:54:45.059Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/42/ec/de55391858b49e16e1aa8f0bbbb7e5997b7345d8e984a2dec3746d13065b/orjson-3.11.5-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7cce16ae2f5fb2c53c3eafdd1706cb7b6530a67cc1c17abe8ec747f5cd7c0c51", size = 135964, upload-time = "2025-12-06T15:54:46.576Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/1c/40/820bc63121d2d28818556a2d0a09384a9f0262407cf9fa305e091a8048df/orjson-3.11.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e46c762d9f0e1cfb4ccc8515de7f349abbc95b59cb5a2bd68df5973fdef913f8", size = 139817, upload-time = "2025-12-06T15:54:48.084Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/09/c7/3a445ca9a84a0d59d26365fd8898ff52bdfcdcb825bcc6519830371d2364/orjson-3.11.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d7345c759276b798ccd6d77a87136029e71e66a8bbf2d2755cbdde1d82e78706", size = 137336, upload-time = "2025-12-06T15:54:49.426Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/9a/b3/dc0d3771f2e5d1f13368f56b339c6782f955c6a20b50465a91acb79fe961/orjson-3.11.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75bc2e59e6a2ac1dd28901d07115abdebc4563b5b07dd612bf64260a201b1c7f", size = 138993, upload-time = "2025-12-06T15:54:50.939Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d1/a2/65267e959de6abe23444659b6e19c888f242bf7725ff927e2292776f6b89/orjson-3.11.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:54aae9b654554c3b4edd61896b978568c6daa16af96fa4681c9b5babd469f863", size = 141070, upload-time = "2025-12-06T15:54:52.414Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/63/c9/da44a321b288727a322c6ab17e1754195708786a04f4f9d2220a5076a649/orjson-3.11.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:4bdd8d164a871c4ec773f9de0f6fe8769c2d6727879c37a9666ba4183b7f8228", size = 413505, upload-time = "2025-12-06T15:54:53.67Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/7f/17/68dc14fa7000eefb3d4d6d7326a190c99bb65e319f02747ef3ebf2452f12/orjson-3.11.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:a261fef929bcf98a60713bf5e95ad067cea16ae345d9a35034e73c3990e927d2", size = 151342, upload-time = "2025-12-06T15:54:55.113Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c4/c5/ccee774b67225bed630a57478529fc026eda33d94fe4c0eac8fe58d4aa52/orjson-3.11.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c028a394c766693c5c9909dec76b24f37e6a1b91999e8d0c0d5feecbe93c3e05", size = 141823, upload-time = "2025-12-06T15:54:56.331Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/67/80/5d00e4155d0cd7390ae2087130637671da713959bb558db9bac5e6f6b042/orjson-3.11.5-cp313-cp313-win32.whl", hash = "sha256:2cc79aaad1dfabe1bd2d50ee09814a1253164b3da4c00a78c458d82d04b3bdef", size = 135236, upload-time = "2025-12-06T15:54:57.507Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/95/fe/792cc06a84808dbdc20ac6eab6811c53091b42f8e51ecebf14b540e9cfe4/orjson-3.11.5-cp313-cp313-win_amd64.whl", hash = "sha256:ff7877d376add4e16b274e35a3f58b7f37b362abf4aa31863dadacdd20e3a583", size = 133167, upload-time = "2025-12-06T15:54:58.71Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/46/2c/d158bd8b50e3b1cfdcf406a7e463f6ffe3f0d167b99634717acdaf5e299f/orjson-3.11.5-cp313-cp313-win_arm64.whl", hash = "sha256:59ac72ea775c88b163ba8d21b0177628bd015c5dd060647bbab6e22da3aad287", size = 126712, upload-time = "2025-12-06T15:54:59.892Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c2/60/77d7b839e317ead7bb225d55bb50f7ea75f47afc489c81199befc5435b50/orjson-3.11.5-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:e446a8ea0a4c366ceafc7d97067bfd55292969143b57e3c846d87fc701e797a0", size = 245252, upload-time = "2025-12-06T15:55:01.127Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f1/aa/d4639163b400f8044cef0fb9aa51b0337be0da3a27187a20d1166e742370/orjson-3.11.5-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:53deb5addae9c22bbe3739298f5f2196afa881ea75944e7720681c7080909a81", size = 129419, upload-time = "2025-12-06T15:55:02.723Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/30/94/9eabf94f2e11c671111139edf5ec410d2f21e6feee717804f7e8872d883f/orjson-3.11.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82cd00d49d6063d2b8791da5d4f9d20539c5951f965e45ccf4e96d33505ce68f", size = 132050, upload-time = "2025-12-06T15:55:03.918Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/3d/c8/ca10f5c5322f341ea9a9f1097e140be17a88f88d1cfdd29df522970d9744/orjson-3.11.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3fd15f9fc8c203aeceff4fda211157fad114dde66e92e24097b3647a08f4ee9e", size = 130370, upload-time = "2025-12-06T15:55:05.173Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/25/d4/e96824476d361ee2edd5c6290ceb8d7edf88d81148a6ce172fc00278ca7f/orjson-3.11.5-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9df95000fbe6777bf9820ae82ab7578e8662051bb5f83d71a28992f539d2cda7", size = 136012, upload-time = "2025-12-06T15:55:06.402Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/85/8e/9bc3423308c425c588903f2d103cfcfe2539e07a25d6522900645a6f257f/orjson-3.11.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:92a8d676748fca47ade5bc3da7430ed7767afe51b2f8100e3cd65e151c0eaceb", size = 139809, upload-time = "2025-12-06T15:55:07.656Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e9/3c/b404e94e0b02a232b957c54643ce68d0268dacb67ac33ffdee24008c8b27/orjson-3.11.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aa0f513be38b40234c77975e68805506cad5d57b3dfd8fe3baa7f4f4051e15b4", size = 137332, upload-time = "2025-12-06T15:55:08.961Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/51/30/cc2d69d5ce0ad9b84811cdf4a0cd5362ac27205a921da524ff42f26d65e0/orjson-3.11.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa1863e75b92891f553b7922ce4ee10ed06db061e104f2b7815de80cdcb135ad", size = 138983, upload-time = "2025-12-06T15:55:10.595Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/0e/87/de3223944a3e297d4707d2fe3b1ffb71437550e165eaf0ca8bbe43ccbcb1/orjson-3.11.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d4be86b58e9ea262617b8ca6251a2f0d63cc132a6da4b5fcc8e0a4128782c829", size = 141069, upload-time = "2025-12-06T15:55:11.832Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/65/30/81d5087ae74be33bcae3ff2d80f5ccaa4a8fedc6d39bf65a427a95b8977f/orjson-3.11.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:b923c1c13fa02084eb38c9c065afd860a5cff58026813319a06949c3af5732ac", size = 413491, upload-time = "2025-12-06T15:55:13.314Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d0/6f/f6058c21e2fc1efaf918986dbc2da5cd38044f1a2d4b7b91ad17c4acf786/orjson-3.11.5-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:1b6bd351202b2cd987f35a13b5e16471cf4d952b42a73c391cc537974c43ef6d", size = 151375, upload-time = "2025-12-06T15:55:14.715Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/54/92/c6921f17d45e110892899a7a563a925b2273d929959ce2ad89e2525b885b/orjson-3.11.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:bb150d529637d541e6af06bbe3d02f5498d628b7f98267ff87647584293ab439", size = 141850, upload-time = "2025-12-06T15:55:15.94Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/88/86/cdecb0140a05e1a477b81f24739da93b25070ee01ce7f7242f44a6437594/orjson-3.11.5-cp314-cp314-win32.whl", hash = "sha256:9cc1e55c884921434a84a0c3dd2699eb9f92e7b441d7f53f3941079ec6ce7499", size = 135278, upload-time = "2025-12-06T15:55:17.202Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e4/97/b638d69b1e947d24f6109216997e38922d54dcdcdb1b11c18d7efd2d3c59/orjson-3.11.5-cp314-cp314-win_amd64.whl", hash = "sha256:a4f3cb2d874e03bc7767c8f88adaa1a9a05cecea3712649c3b58589ec7317310", size = 133170, upload-time = "2025-12-06T15:55:18.468Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/8f/dd/f4fff4a6fe601b4f8f3ba3aa6da8ac33d17d124491a3b804c662a70e1636/orjson-3.11.5-cp314-cp314-win_arm64.whl", hash = "sha256:38b22f476c351f9a1c43e5b07d8b5a02eb24a6ab8e75f700f7d479d4568346a5", size = 126713, upload-time = "2025-12-06T15:55:19.738Z" }, -] - [[package]] name = "packaging" version = "25.0" @@ -1884,28 +1169,13 @@ name = "pandas" version = "2.2.3" source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } dependencies = [ - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.4.1", source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy" }, { name = "python-dateutil" }, { name = "pytz" }, { name = "tzdata" }, ] sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/9c/d6/9f8431bacc2e19dca897724cd097b1bb224a6ad5433784a44b587c7c13af/pandas-2.2.3.tar.gz", hash = "sha256:4f18ba62b61d7e192368b84517265a99b4d7ee8912f8708660fb4a366cc82667", size = 4399213, upload-time = "2024-09-20T13:10:04.827Z" } wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/aa/70/c853aec59839bceed032d52010ff5f1b8d87dc3114b762e4ba2727661a3b/pandas-2.2.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1948ddde24197a0f7add2bdc4ca83bf2b1ef84a1bc8ccffd95eda17fd836ecb5", size = 12580827, upload-time = "2024-09-20T13:08:42.347Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/99/f2/c4527768739ffa4469b2b4fff05aa3768a478aed89a2f271a79a40eee984/pandas-2.2.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:381175499d3802cde0eabbaf6324cce0c4f5d52ca6f8c377c29ad442f50f6348", size = 11303897, upload-time = "2024-09-20T13:08:45.807Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ed/12/86c1747ea27989d7a4064f806ce2bae2c6d575b950be087837bdfcabacc9/pandas-2.2.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d9c45366def9a3dd85a6454c0e7908f2b3b8e9c138f5dc38fed7ce720d8453ed", size = 66480908, upload-time = "2024-09-20T18:37:13.513Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/44/50/7db2cd5e6373ae796f0ddad3675268c8d59fb6076e66f0c339d61cea886b/pandas-2.2.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86976a1c5b25ae3f8ccae3a5306e443569ee3c3faf444dfd0f41cda24667ad57", size = 13064210, upload-time = "2024-09-20T13:08:48.325Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/61/61/a89015a6d5536cb0d6c3ba02cebed51a95538cf83472975275e28ebf7d0c/pandas-2.2.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b8661b0238a69d7aafe156b7fa86c44b881387509653fdf857bebc5e4008ad42", size = 16754292, upload-time = "2024-09-20T19:01:54.443Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ce/0d/4cc7b69ce37fac07645a94e1d4b0880b15999494372c1523508511b09e40/pandas-2.2.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:37e0aced3e8f539eccf2e099f65cdb9c8aa85109b0be6e93e2baff94264bdc6f", size = 14416379, upload-time = "2024-09-20T13:08:50.882Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/31/9e/6ebb433de864a6cd45716af52a4d7a8c3c9aaf3a98368e61db9e69e69a9c/pandas-2.2.3-cp310-cp310-win_amd64.whl", hash = "sha256:56534ce0746a58afaf7942ba4863e0ef81c9c50d3f0ae93e9497d6a41a057645", size = 11598471, upload-time = "2024-09-20T13:08:53.332Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a8/44/d9502bf0ed197ba9bf1103c9867d5904ddcaf869e52329787fc54ed70cc8/pandas-2.2.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:66108071e1b935240e74525006034333f98bcdb87ea116de573a6a0dccb6c039", size = 12602222, upload-time = "2024-09-20T13:08:56.254Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/52/11/9eac327a38834f162b8250aab32a6781339c69afe7574368fffe46387edf/pandas-2.2.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7c2875855b0ff77b2a64a0365e24455d9990730d6431b9e0ee18ad8acee13dbd", size = 11321274, upload-time = "2024-09-20T13:08:58.645Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/45/fb/c4beeb084718598ba19aa9f5abbc8aed8b42f90930da861fcb1acdb54c3a/pandas-2.2.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd8d0c3be0515c12fed0bdbae072551c8b54b7192c7b1fda0ba56059a0179698", size = 15579836, upload-time = "2024-09-20T19:01:57.571Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/cd/5f/4dba1d39bb9c38d574a9a22548c540177f78ea47b32f99c0ff2ec499fac5/pandas-2.2.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c124333816c3a9b03fbeef3a9f230ba9a737e9e5bb4060aa2107a86cc0a497fc", size = 13058505, upload-time = "2024-09-20T13:09:01.501Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b9/57/708135b90391995361636634df1f1130d03ba456e95bcf576fada459115a/pandas-2.2.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:63cc132e40a2e084cf01adf0775b15ac515ba905d7dcca47e9a251819c575ef3", size = 16744420, upload-time = "2024-09-20T19:02:00.678Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/86/4a/03ed6b7ee323cf30404265c284cee9c65c56a212e0a08d9ee06984ba2240/pandas-2.2.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:29401dbfa9ad77319367d36940cd8a0b3a11aba16063e39632d98b0e931ddf32", size = 14440457, upload-time = "2024-09-20T13:09:04.105Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ed/8c/87ddf1fcb55d11f9f847e3c69bb1c6f8e46e2f40ab1a2d2abadb2401b007/pandas-2.2.3-cp311-cp311-win_amd64.whl", hash = "sha256:3fc6873a41186404dad67245896a6e440baacc92f5b716ccd1bc9ed2995ab2c5", size = 11617166, upload-time = "2024-09-20T13:09:06.917Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/17/a3/fb2734118db0af37ea7433f57f722c0a56687e14b14690edff0cdb4b7e58/pandas-2.2.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b1d432e8d08679a40e2a6d8b2f9770a5c21793a6f9f47fdd52c5ce1948a5a8a9", size = 12529893, upload-time = "2024-09-20T13:09:09.655Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e1/0c/ad295fd74bfac85358fd579e271cded3ac969de81f62dd0142c426b9da91/pandas-2.2.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a5a1595fe639f5988ba6a8e5bc9649af3baf26df3998a0abe56c02609392e0a4", size = 11363475, upload-time = "2024-09-20T13:09:14.718Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c6/2a/4bba3f03f7d07207481fed47f5b35f556c7441acddc368ec43d6643c5777/pandas-2.2.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5de54125a92bb4d1c051c0659e6fcb75256bf799a732a87184e5ea503965bce3", size = 15188645, upload-time = "2024-09-20T19:02:03.88Z" }, @@ -1937,15 +1207,6 @@ wheels = [ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/3b/a4/ab6b7589382ca3df236e03faa71deac88cae040af60c071a78d254a62172/passlib-1.7.4-py2.py3-none-any.whl", hash = "sha256:aa6bca462b8d8bda89c70b382f0c298a20b5560af6cbfa2dce410c0a2fb669f1", size = 525554, upload-time = "2020-10-08T19:00:49.856Z" }, ] -[[package]] -name = "pathspec" -version = "1.0.4" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/fa/36/e27608899f9b8d4dff0617b2d9ab17ca5608956ca44461ac14ac48b44015/pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645", size = 131200, upload-time = "2026-01-27T03:59:46.938Z" } -wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206, upload-time = "2026-01-27T03:59:45.137Z" }, -] - [[package]] name = "pendulum" version = "3.2.0" @@ -1956,25 +1217,6 @@ dependencies = [ ] sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/cb/72/9a51afa0a822b09e286c4cb827ed7b00bc818dac7bd11a5f161e493a217d/pendulum-3.2.0.tar.gz", hash = "sha256:e80feda2d10fa3ff8b1526715f7d33dcb7e08494b3088f2c8a3ac92d4a4331ce", size = 86912, upload-time = "2026-01-30T11:22:24.093Z" } wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ca/a4/934d8c97851bda5a034b0fd0512689173c8ca8cb3b87ebf8e5c1364d57f3/pendulum-3.2.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:4a6bf778c6b42830b001c714dae5b9dad78da38e2e08203a4b0f5d53f8fa5e63", size = 338065, upload-time = "2026-01-30T11:20:36.467Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/47/92/2091275a9025f9b9ef9bf72ae386786a9b03af9515f5e2f5befb012ec91f/pendulum-3.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:625209bb7133d990905e8935e1c04f0a82315ae777b67910969b16f665d62c0b", size = 327426, upload-time = "2026-01-30T11:20:38.506Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/fd/ba/efc999e5b441a470df28964531c3ee7fce90dd2c510969132bba5897084e/pendulum-3.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b6f1d8641e8bd48b9b6f77f96fd498d3ecec63611ba8e7207e63936307846042", size = 340362, upload-time = "2026-01-30T11:20:40.329Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/8c/71/bc88d786f0a10fcfdc5a0bac75c6cdb38df13ee09bc04d2e6ac0d3fd7948/pendulum-3.2.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3a8d4212b1577ee3a034d18b360a9afa55bfc72789aeb805353be8b2ac132035", size = 373937, upload-time = "2026-01-30T11:20:42.242Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/86/fb/48262b5b31fdfd68221cb92ab228657d0cd628fb35eca1a6f3aedad5ea09/pendulum-3.2.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e398d9e3db42d17f0c2cd39663c1c873ea6f11763ed6d126e2dcc92fc340d0dc", size = 379391, upload-time = "2026-01-30T11:20:43.736Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ae/72/cecb1710c36c6fe61e545050607c2050a2af0b991cf1a3d83981dfd895e8/pendulum-3.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:04310463879a8d84534756ef9820d433e88b879203b6e10a5b416899dc05e7f1", size = 348433, upload-time = "2026-01-30T11:20:45.207Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/5d/a5/ec00008ba2f3298047a32b53588550a7ead84c579e7d7e4396474ab2f1ef/pendulum-3.2.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:5b4f7491951c11bbdb20893817352c9140d31d1ae333839c34c0bca081a50a86", size = 517623, upload-time = "2026-01-30T11:20:46.741Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f1/6f/541730ac4679e7f7ff5786aed21865c4f4a7d9b1d2693cfdbb891bdd5a5a/pendulum-3.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ffc169ad7595228d4dfc44d4e016846ff1bb5873b9f7ec70b0b1b51da0c77b3f", size = 561237, upload-time = "2026-01-30T11:20:48.252Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/cf/c1/165f10f2e37978caf92a1dca71726e7cd5d8de4039f9f4a6d1994a9b8d7f/pendulum-3.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:446f63d84ef21281844ceb45141536d3aabe291a821b6505e21a0d0e3ea95d67", size = 260733, upload-time = "2026-01-30T11:20:50.249Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c4/27/a4be6ec12161b503dd036f8d7cc57f8626170ae31bb298038be9af0001ce/pendulum-3.2.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:5d775cc608c909ad415c8e789c84a9f120bb6a794c4215b2d8d910893cf0ec6a", size = 337923, upload-time = "2026-01-30T11:20:51.61Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/59/e1/2a214e18355ec2a6ce3f683a97eecdb6050866ff3a6cf165d411450aeb1b/pendulum-3.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8de794a7f665aebc8c1ba4dd4b05ab8fe1a36ce9c0498366adf1d1edd79b2686", size = 327379, upload-time = "2026-01-30T11:20:53.085Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/9d/01/7392e58ebc1d9e70b987dc8bb0c89710b47ac8125067efe7aa4c420b616f/pendulum-3.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bac7df7696e1c942e17c0556b3a7bcdd1d7aa5b24faee7620cb071e754a0622", size = 340115, upload-time = "2026-01-30T11:20:54.635Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ef/33/80de84c5ca1a3e4f7f3b75090c9b61b6dbb6d095e302ee592cebbaf0bbfb/pendulum-3.2.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:db0f6a8a04475d9cba26ce701e7d66d266fd97227f2f5f499270eba04be1c7e9", size = 373969, upload-time = "2026-01-30T11:20:56.209Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/75/e4/f7b4c1818927ab394a2a0a9b7011f360a0a75839a22678833c5bc0a84183/pendulum-3.2.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c352c63c1ff05f2198409b28498d7158547a8be23e1fbd4aa2cf5402fb239b55", size = 379058, upload-time = "2026-01-30T11:20:57.618Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/36/94/9947cf710620afcc68751683f2f8de88d902505e7c13c0349d7e9d362f97/pendulum-3.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de8c1ad1d1aa7d4ceae341528bab35a0f8c88a5aa63f2f5d84e16b517d1b32c2", size = 348403, upload-time = "2026-01-30T11:20:59.56Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/6f/12/0e6ba0bb00fa57907af2a3fca8643bded5dba1e87072d50673776a0d6ed2/pendulum-3.2.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1ba955511c12fec2252038b0c866c25c0c30b720bf74d3023710f121e42b1498", size = 517457, upload-time = "2026-01-30T11:21:01.602Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c6/fe/dae5fbfe67bd41d943def0ad8f1e7f6988aa8e527255e433cd7c494f9ad5/pendulum-3.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:4115bf364a2ec6d5ddc476751ceaa4164a04f2c15589f0d29aa210ddb784b15d", size = 561103, upload-time = "2026-01-30T11:21:03.924Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ce/a0/8f646160b98abfc19152505af19bd643a4279ec2bdbe0959f16b7025fc6b/pendulum-3.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:4151a903356413fdd9549de0997b708fb95a214ed97803ffb479ffd834088378", size = 260595, upload-time = "2026-01-30T11:21:05.495Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/79/01/feead7af9ded7a13f2d798fb6573e70f469113eafcd8cc8f59671584ca3e/pendulum-3.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:acfdee9ddc56053cb7c8c075afbfde0857322d09e56a56195b9cd127fae87e4c", size = 255382, upload-time = "2026-01-30T11:21:06.847Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/41/56/dd0ea9f97d25a0763cda09e2217563b45714786118d8c68b0b745395d6eb/pendulum-3.2.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:bf0b489def51202a39a2a665dcc4162d5e46934a740fe4c4fe3068979610156c", size = 337830, upload-time = "2026-01-30T11:21:08.298Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/cf/98/83d62899bf7226fc12396de4bc1fb2b5da27e451c7c60790043aaf8b4731/pendulum-3.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:937a529aa302efa18dcf25e53834964a87ffb2df8f80e3669ab7757a6126beaf", size = 327574, upload-time = "2026-01-30T11:21:09.715Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/76/fa/ff2aa992b23f0543c709b1a3f3f9ed760ec71fd02c8bb01f93bf008b52e4/pendulum-3.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85c7689defc65c4dc29bf257f7cca55d210fabb455de9476e1748d2ab2ae80d7", size = 339891, upload-time = "2026-01-30T11:21:11.089Z" }, @@ -2005,13 +1247,6 @@ wheels = [ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c9/37/b4f2b5f1200351c4869b8b46ad5c21019e3dbe0417f5867ae969fad7b5fe/pendulum-3.2.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:a50d8cf42f06d3d8c3f8bb2a7ac47fa93b5145e69de6a7209be6a47afdd9cf76", size = 561926, upload-time = "2026-01-30T11:21:51.698Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a0/9e/567376582da58f5fe8e4f579db2bcfbf243cf619a5825bdf1023ad1436b3/pendulum-3.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:e5bbb92b155cd5018b3cf70ee49ed3b9c94398caaaa7ed97fe41e5bb5a968418", size = 258817, upload-time = "2026-01-30T11:21:53.074Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/95/67/dfffd7eb50d67fa821cd4d92cf71575ead6162930202bc40dfcedf78c38c/pendulum-3.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:d53134418e04335c3029a32e9341cccc9b085a28744fb5ee4e6a8f5039363b1a", size = 253292, upload-time = "2026-01-30T11:21:54.484Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c9/0d/d5ac8468a1b40f09a62d6e91654088de432367907579dd161c0fb1bdf222/pendulum-3.2.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:9585594d32faa71efa5a78f576f1ee4f79e9c5340d7c6f0cd6c5dfe725effaaa", size = 338760, upload-time = "2026-01-30T11:22:12.225Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a0/e5/7fa8c8be6caac8e0be78fbe7668df571f44820ed779cb3736fab645fcba8/pendulum-3.2.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:26401e2de77c437e8f3b6160c08c6c5d45518d906f8f9b48fd7cb5aa0f4e2aff", size = 328333, upload-time = "2026-01-30T11:22:13.811Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ad/78/73a1031b7d1bf7986e8e655cea3f018164b3470aecfea25a4074e77dda73/pendulum-3.2.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:637e65af042f383a2764a886aa28ccc6f853bf7a142df18e41c720542934c13b", size = 340841, upload-time = "2026-01-30T11:22:15.278Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/49/40/4e36e9074e92b0164c088b9ada3c02bfea386d83e24fa98b30fe9b6e61a8/pendulum-3.2.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d6e46c28f4d067233c4a4c42748f4ffa641d9289c09e0e81488beb6d4b3fab51", size = 348959, upload-time = "2026-01-30T11:22:16.718Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/24/99/8bf7fcb91b526e1efe17d047faa845709b88800fff915ff848ff26054293/pendulum-3.2.0-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:71d46bcc86269f97bfd8c5f1475d55e717696a0a010b1871023605ca94624031", size = 518102, upload-time = "2026-01-30T11:22:18.2Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b8/b0/a36c468d2d0dec62ddea7c5e4177e93abb12f48ac90f09f24d0581c5189f/pendulum-3.2.0-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:5cd956d4176afc7bfe8a91bf3f771b46ff8d326f6c5bf778eb5010eb742ebba6", size = 561884, upload-time = "2026-01-30T11:22:19.671Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c5/4d/dad105261898907bf806cabca53d3878529a9fa2c0d5d7f95f2035246fc2/pendulum-3.2.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:39ef129d7b90aab49708645867abdd207b714ba7bff12dae549975b0aca09716", size = 261236, upload-time = "2026-01-30T11:22:21.059Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/02/fb/d65db067a67df7252f18b0cb7420dda84078b9e8bfb375215469c14a50be/pendulum-3.2.0-py3-none-any.whl", hash = "sha256:f3a9c18a89b4d9ef39c5fa6a78722aaff8d5be2597c129a3b16b9f40a561acf3", size = 114111, upload-time = "2026-01-30T11:22:22.361Z" }, ] @@ -2021,28 +1256,6 @@ version = "11.0.0" source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a5/26/0d95c04c868f6bdb0c447e3ee2de5564411845e36a858cfd63766bc7b563/pillow-11.0.0.tar.gz", hash = "sha256:72bacbaf24ac003fea9bff9837d1eedb6088758d41e100c1552930151f677739", size = 46737780, upload-time = "2024-10-15T14:24:29.672Z" } wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/98/fb/a6ce6836bd7fd93fbf9144bf54789e02babc27403b50a9e1583ee877d6da/pillow-11.0.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:6619654954dc4936fcff82db8eb6401d3159ec6be81e33c6000dfd76ae189947", size = 3154708, upload-time = "2024-10-15T14:21:49.832Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/6a/1d/1f51e6e912d8ff316bb3935a8cda617c801783e0b998bf7a894e91d3bd4c/pillow-11.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b3c5ac4bed7519088103d9450a1107f76308ecf91d6dabc8a33a2fcfb18d0fba", size = 2979223, upload-time = "2024-10-15T14:21:53.265Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/90/83/e2077b0192ca8a9ef794dbb74700c7e48384706467067976c2a95a0f40a1/pillow-11.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a65149d8ada1055029fcb665452b2814fe7d7082fcb0c5bed6db851cb69b2086", size = 4183167, upload-time = "2024-10-15T14:21:55.475Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/0e/74/467af0146970a98349cdf39e9b79a6cc8a2e7558f2c01c28a7b6b85c5bda/pillow-11.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88a58d8ac0cc0e7f3a014509f0455248a76629ca9b604eca7dc5927cc593c5e9", size = 4283912, upload-time = "2024-10-15T14:21:57.799Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/85/b1/d95d4f7ca3a6c1ae120959605875a31a3c209c4e50f0029dc1a87566cf46/pillow-11.0.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:c26845094b1af3c91852745ae78e3ea47abf3dbcd1cf962f16b9a5fbe3ee8488", size = 4195815, upload-time = "2024-10-15T14:22:00.112Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/41/c3/94f33af0762ed76b5a237c5797e088aa57f2b7fa8ee7932d399087be66a8/pillow-11.0.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:1a61b54f87ab5786b8479f81c4b11f4d61702830354520837f8cc791ebba0f5f", size = 4366117, upload-time = "2024-10-15T14:22:02.556Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ba/3c/443e7ef01f597497268899e1cca95c0de947c9bbf77a8f18b3c126681e5d/pillow-11.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:674629ff60030d144b7bca2b8330225a9b11c482ed408813924619c6f302fdbb", size = 4278607, upload-time = "2024-10-15T14:22:04.682Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/26/95/1495304448b0081e60c0c5d63f928ef48bb290acee7385804426fa395a21/pillow-11.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:598b4e238f13276e0008299bd2482003f48158e2b11826862b1eb2ad7c768b97", size = 4410685, upload-time = "2024-10-15T14:22:06.767Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/45/da/861e1df971ef0de9870720cb309ca4d553b26a9483ec9be3a7bf1de4a095/pillow-11.0.0-cp310-cp310-win32.whl", hash = "sha256:9a0f748eaa434a41fccf8e1ee7a3eed68af1b690e75328fd7a60af123c193b50", size = 2249185, upload-time = "2024-10-15T14:22:08.449Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d5/4e/78f7c5202ea2a772a5ab05069c1b82503e6353cd79c7e474d4945f4b82c3/pillow-11.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:a5629742881bcbc1f42e840af185fd4d83a5edeb96475a575f4da50d6ede337c", size = 2566726, upload-time = "2024-10-15T14:22:11.368Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/77/e4/6e84eada35cbcc646fc1870f72ccfd4afacb0fae0c37ffbffe7f5dc24bf1/pillow-11.0.0-cp310-cp310-win_arm64.whl", hash = "sha256:ee217c198f2e41f184f3869f3e485557296d505b5195c513b2bfe0062dc537f1", size = 2254585, upload-time = "2024-10-15T14:22:13.521Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f0/eb/f7e21b113dd48a9c97d364e0915b3988c6a0b6207652f5a92372871b7aa4/pillow-11.0.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:1c1d72714f429a521d8d2d018badc42414c3077eb187a59579f28e4270b4b0fc", size = 3154705, upload-time = "2024-10-15T14:22:15.419Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/25/b3/2b54a1d541accebe6bd8b1358b34ceb2c509f51cb7dcda8687362490da5b/pillow-11.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:499c3a1b0d6fc8213519e193796eb1a86a1be4b1877d678b30f83fd979811d1a", size = 2979222, upload-time = "2024-10-15T14:22:17.681Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/20/12/1a41eddad8265c5c19dda8fb6c269ce15ee25e0b9f8f26286e6202df6693/pillow-11.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c8b2351c85d855293a299038e1f89db92a2f35e8d2f783489c6f0b2b5f3fe8a3", size = 4190220, upload-time = "2024-10-15T14:22:19.826Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a9/9b/8a8c4d07d77447b7457164b861d18f5a31ae6418ef5c07f6f878fa09039a/pillow-11.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f4dba50cfa56f910241eb7f883c20f1e7b1d8f7d91c750cd0b318bad443f4d5", size = 4291399, upload-time = "2024-10-15T14:22:22.129Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/fc/e4/130c5fab4a54d3991129800dd2801feeb4b118d7630148cd67f0e6269d4c/pillow-11.0.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:5ddbfd761ee00c12ee1be86c9c0683ecf5bb14c9772ddbd782085779a63dd55b", size = 4202709, upload-time = "2024-10-15T14:22:23.953Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/39/63/b3fc299528d7df1f678b0666002b37affe6b8751225c3d9c12cf530e73ed/pillow-11.0.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:45c566eb10b8967d71bf1ab8e4a525e5a93519e29ea071459ce517f6b903d7fa", size = 4372556, upload-time = "2024-10-15T14:22:25.706Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c6/a6/694122c55b855b586c26c694937d36bb8d3b09c735ff41b2f315c6e66a10/pillow-11.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b4fd7bd29610a83a8c9b564d457cf5bd92b4e11e79a4ee4716a63c959699b306", size = 4287187, upload-time = "2024-10-15T14:22:27.362Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ba/a9/f9d763e2671a8acd53d29b1e284ca298bc10a595527f6be30233cdb9659d/pillow-11.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:cb929ca942d0ec4fac404cbf520ee6cac37bf35be479b970c4ffadf2b6a1cad9", size = 4418468, upload-time = "2024-10-15T14:22:29.093Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/6e/0e/b5cbad2621377f11313a94aeb44ca55a9639adabcaaa073597a1925f8c26/pillow-11.0.0-cp311-cp311-win32.whl", hash = "sha256:006bcdd307cc47ba43e924099a038cbf9591062e6c50e570819743f5607404f5", size = 2249249, upload-time = "2024-10-15T14:22:31.268Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/dc/83/1470c220a4ff06cd75fc609068f6605e567ea51df70557555c2ab6516b2c/pillow-11.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:52a2d8323a465f84faaba5236567d212c3668f2ab53e1c74c15583cf507a0291", size = 2566769, upload-time = "2024-10-15T14:22:32.974Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/52/98/def78c3a23acee2bcdb2e52005fb2810ed54305602ec1bfcfab2bda6f49f/pillow-11.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:16095692a253047fe3ec028e951fa4221a1f3ed3d80c397e83541a3037ff67c9", size = 2254611, upload-time = "2024-10-15T14:22:35.496Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/1c/a3/26e606ff0b2daaf120543e537311fa3ae2eb6bf061490e4fea51771540be/pillow-11.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d2c0a187a92a1cb5ef2c8ed5412dd8d4334272617f532d4ad4de31e0495bd923", size = 3147642, upload-time = "2024-10-15T14:22:37.736Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/4f/d5/1caabedd8863526a6cfa44ee7a833bd97f945dc1d56824d6d76e11731939/pillow-11.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:084a07ef0821cfe4858fe86652fffac8e187b6ae677e9906e192aafcc1b69903", size = 2978999, upload-time = "2024-10-15T14:22:39.654Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d9/ff/5a45000826a1aa1ac6874b3ec5a856474821a1b59d838c4f6ce2ee518fe9/pillow-11.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8069c5179902dcdce0be9bfc8235347fdbac249d23bd90514b7a47a72d9fecf4", size = 4196794, upload-time = "2024-10-15T14:22:41.598Z" }, @@ -2073,13 +1286,6 @@ wheels = [ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d6/b9/fb620dd47fc7cc9678af8f8bd8c772034ca4977237049287e99dda360b66/pillow-11.0.0-cp313-cp313t-win32.whl", hash = "sha256:607bbe123c74e272e381a8d1957083a9463401f7bd01287f50521ecb05a313f8", size = 2253197, upload-time = "2024-10-15T14:23:35.309Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/df/86/25dde85c06c89d7fc5db17940f07aae0a56ac69aa9ccb5eb0f09798862a8/pillow-11.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5c39ed17edea3bc69c743a8dd3e9853b7509625c2462532e62baa0732163a904", size = 2572169, upload-time = "2024-10-15T14:23:37.33Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/51/85/9c33f2517add612e17f3381aee7c4072779130c634921a756c97bc29fb49/pillow-11.0.0-cp313-cp313t-win_arm64.whl", hash = "sha256:75acbbeb05b86bc53cbe7b7e6fe00fbcf82ad7c684b3ad82e3d711da9ba287d3", size = 2256828, upload-time = "2024-10-15T14:23:39.826Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/36/57/42a4dd825eab762ba9e690d696d894ba366e06791936056e26e099398cda/pillow-11.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1187739620f2b365de756ce086fdb3604573337cc28a0d3ac4a01ab6b2d2a6d2", size = 3119239, upload-time = "2024-10-15T14:24:06.042Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/98/f7/25f9f9e368226a1d6cf3507081a1a7944eddd3ca7821023377043f5a83c8/pillow-11.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:fbbcb7b57dc9c794843e3d1258c0fbf0f48656d46ffe9e09b63bbd6e8cd5d0a2", size = 2950803, upload-time = "2024-10-15T14:24:08.068Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/59/01/98ead48a6c2e31e6185d4c16c978a67fe3ccb5da5c2ff2ba8475379bb693/pillow-11.0.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d203af30149ae339ad1b4f710d9844ed8796e97fda23ffbc4cc472968a47d0b", size = 3281098, upload-time = "2024-10-15T14:24:10.01Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/51/c0/570255b2866a0e4d500a14f950803a2ec273bac7badc43320120b9262450/pillow-11.0.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21a0d3b115009ebb8ac3d2ebec5c2982cc693da935f4ab7bb5c8ebe2f47d36f2", size = 3323665, upload-time = "2024-10-15T14:24:12.213Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/0e/75/689b4ec0483c42bfc7d1aacd32ade7a226db4f4fac57c6fdcdf90c0731e3/pillow-11.0.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:73853108f56df97baf2bb8b522f3578221e56f646ba345a372c78326710d3830", size = 3310533, upload-time = "2024-10-15T14:24:14.563Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/3d/30/38bd6149cf53da1db4bad304c543ade775d225961c4310f30425995cb9ec/pillow-11.0.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e58876c91f97b0952eb766123bfef372792ab3f4e3e1f1a2267834c2ab131734", size = 3414886, upload-time = "2024-10-15T14:24:16.511Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ec/3d/c32a51d848401bd94cabb8767a39621496491ee7cd5199856b77da9b18ad/pillow-11.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:224aaa38177597bb179f3ec87eeefcce8e4f85e608025e9cfac60de237ba6316", size = 2567508, upload-time = "2024-10-15T14:24:18.616Z" }, ] [[package]] @@ -2091,82 +1297,6 @@ wheels = [ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, ] -[[package]] -name = "prefect" -version = "3.6.24" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } -dependencies = [ - { name = "aiosqlite" }, - { name = "alembic" }, - { name = "amplitude-analytics" }, - { name = "anyio" }, - { name = "apprise" }, - { name = "asgi-lifespan" }, - { name = "asyncpg" }, - { name = "cachetools" }, - { name = "click" }, - { name = "cloudpickle" }, - { name = "coolname" }, - { name = "cryptography" }, - { name = "cyclopts" }, - { name = "dateparser" }, - { name = "docker" }, - { name = "exceptiongroup" }, - { name = "fastapi" }, - { name = "fsspec" }, - { name = "graphviz" }, - { name = "griffe" }, - { name = "httpcore" }, - { name = "httpx", extra = ["http2"] }, - { name = "humanize" }, - { name = "jinja2" }, - { name = "jinja2-humanize-extension" }, - { name = "jsonpatch" }, - { name = "jsonschema" }, - { name = "opentelemetry-api" }, - { name = "orjson" }, - { name = "packaging" }, - { name = "pathspec" }, - { name = "pendulum", marker = "python_full_version < '3.13'" }, - { name = "pluggy" }, - { name = "prometheus-client" }, - { name = "pydantic" }, - { name = "pydantic-core" }, - { name = "pydantic-extra-types" }, - { name = "pydantic-settings" }, - { name = "pydocket" }, - { name = "python-dateutil" }, - { name = "python-slugify" }, - { name = "pytz" }, - { name = "pyyaml" }, - { name = "readchar" }, - { name = "rfc3339-validator" }, - { name = "rich" }, - { name = "ruamel-yaml" }, - { name = "ruamel-yaml-clib", marker = "platform_python_implementation == 'CPython'" }, - { name = "semver" }, - { name = "sniffio" }, - { name = "sqlalchemy", extra = ["asyncio"] }, - { name = "toml" }, - { name = "typing-extensions" }, - { name = "uvicorn" }, - { name = "websockets" }, - { name = "whenever", marker = "python_full_version >= '3.13'" }, -] -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d6/55/dfdae6cc87ae3ab06995596374232b66d2480b8b350ce0d54dde157932ee/prefect-3.6.24.tar.gz", hash = "sha256:cef1ebdbdd78daaf6fea156aac795674e94995fed0b9ea839c5ebf041e0a50ae", size = 11016101, upload-time = "2026-03-27T17:53:37.087Z" } -wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c9/34/5c660a1532f15a12defe83d097c55cb28c38872778399e8e9587c31bb3c2/prefect-3.6.24-py3-none-any.whl", hash = "sha256:5ac830a325f8775e8ddfdc27a5962bafccc6f26527f44361c2b1da703363650b", size = 11847864, upload-time = "2026-03-27T17:53:34.153Z" }, -] - -[[package]] -name = "prometheus-client" -version = "0.22.1" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/5e/cf/40dde0a2be27cc1eb41e333d1a674a74ce8b8b0457269cc640fd42b07cf7/prometheus_client-0.22.1.tar.gz", hash = "sha256:190f1331e783cf21eb60bca559354e0a4d4378facecf78f5428c39b675d20d28", size = 69746, upload-time = "2025-06-02T14:29:01.152Z" } -wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/32/ae/ec06af4fe3ee72d16973474f122541746196aaa16cea6f66d18b963c6177/prometheus_client-0.22.1-py3-none-any.whl", hash = "sha256:cca895342e308174341b2cbf99a56bef291fbc0ef7b9e5412a0f26d653ba7094", size = 58694, upload-time = "2025-06-02T14:29:00.068Z" }, -] - [[package]] name = "psutil" version = "6.1.0" @@ -2200,28 +1330,6 @@ name = "psycopg-binary" version = "3.3.2" source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/25/d7/edfb0d9e56081246fd88490f99b1bafebd3588480cca601a4de0c41a3e08/psycopg_binary-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0768c5f32934bb52a5df098317eca9bdcf411de627c5dca2ee57662b64b54b41", size = 4597785, upload-time = "2025-12-06T17:31:44.867Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/71/45/8458201d9573dd851263a05cefddd4bfd31e8b3c6434b3e38d62aea9f15a/psycopg_binary-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:09b3014013f05cd89828640d3a1db5f829cc24ad8fa81b6e42b2c04685a0c9d4", size = 4664440, upload-time = "2025-12-06T17:31:49.1Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d1/33/484260d87456cfe88dc219c1919026f11949b9d1de8a6371ddbe027d4d60/psycopg_binary-3.3.2-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:3789d452a9d17a841c7f4f97bbcba51a21f957ea35641a4c98507520e6b6a068", size = 5478355, upload-time = "2025-12-06T17:31:52.657Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/34/b2/18c91630c30c83f534c2bfa75fb533293fc9c3ab31bb7f2bf1cd9579c53b/psycopg_binary-3.3.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:44e89938d36acc4495735af70a886d206a5bfdc80258f95b69b52f68b2968d9e", size = 5152398, upload-time = "2025-12-06T17:31:56.092Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c0/14/7c705e1934107196d9dca2040cf34bce2ca26de62520e43073d2673052d4/psycopg_binary-3.3.2-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90ed9da805e52985b0202aed4f352842c907c6b4fc6c7c109c6e646c32e2f43b", size = 6748982, upload-time = "2025-12-06T17:32:00.611Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/56/18/80197c47798926f79e563af02a71d1abecab88cf45ddf8dc960700598da7/psycopg_binary-3.3.2-cp310-cp310-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c3a9ccdfee4ae59cf9bf1822777e763bc097ed208f4901e21537fca1070e1391", size = 4991214, upload-time = "2025-12-06T17:32:03.897Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/7e/2e/e88e2f678f5d1a968d87e57b30915061c1157e916b8aaa9b0b78bca95e25/psycopg_binary-3.3.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:de9173f8cc0efd88ac2a89b3b6c287a9a0011cdc2f53b2a12c28d6fd55f9f81c", size = 4517421, upload-time = "2025-12-06T17:32:07.287Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/80/9e/d56813b24370723bcd62bf73871aee4d5fca0536f3476c4c4d5b037e3c7f/psycopg_binary-3.3.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:0611f4822674f3269e507a307236efb62ae5a828fcfc923ac85fe22ca19fd7c8", size = 4206124, upload-time = "2025-12-06T17:32:10.374Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/91/81/5a11a898969edf0ee43d0613a6dfd689a0aa12d418c69e148a8ff153fbc7/psycopg_binary-3.3.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:522b79c7db547767ca923e441c19b97a2157f2f494272a119c854bba4804e186", size = 3937067, upload-time = "2025-12-06T17:32:13.852Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a1/33/a6180ff1e747a0395876d985e8e295c9d7cbe956a2d66f165e7c67cffe55/psycopg_binary-3.3.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1ea41c0229f3f5a3844ad0857a83a9f869aa7b840448fa0c200e6bcf85d33d19", size = 4243731, upload-time = "2025-12-06T17:32:16.803Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e9/5b/9c1b6fbc900d5b525946ed9a477865c5016a5306080c0557248bb04f1a5b/psycopg_binary-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:8ea05b499278790a8fa0ff9854ab0de2542aca02d661ddff94e830df971ff640", size = 3546403, upload-time = "2025-12-06T17:32:19.621Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/57/d9/49640360fc090d27afc4655021544aa71d5393ebae124ffa53a04474b493/psycopg_binary-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:94503b79f7da0b65c80d0dbb2f81dd78b300319ec2435d5e6dcf9622160bc2fa", size = 4597890, upload-time = "2025-12-06T17:32:23.087Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/85/cf/99634bbccc8af0dd86df4bce705eea5540d06bb7f5ab3067446ae9ffdae4/psycopg_binary-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:07a5f030e0902ec3e27d0506ceb01238c0aecbc73ecd7fa0ee55f86134600b5b", size = 4664396, upload-time = "2025-12-06T17:32:26.421Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/40/db/6035dff6d5c6dfca3a4ab0d2ac62ede623646e327e9f99e21e0cf08976c6/psycopg_binary-3.3.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e09d0d93d35c134704a2cb2b15f81ffc8174fd602f3e08f7b1a3d8896156cf0", size = 5478743, upload-time = "2025-12-06T17:32:29.901Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/03/0f/fc06bbc8e87f09458d2ce04a59cd90565e54e8efca33e0802daee6d2b0e6/psycopg_binary-3.3.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:649c1d33bedda431e0c1df646985fbbeb9274afa964e1aef4be053c0f23a2924", size = 5151820, upload-time = "2025-12-06T17:32:33.562Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/86/ab/bcc0397c96a0ad29463e33ed03285826e0fabc43595c195f419d9291ee70/psycopg_binary-3.3.2-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c5774272f754605059521ff037a86e680342e3847498b0aa86b0f3560c70963c", size = 6747711, upload-time = "2025-12-06T17:32:38.074Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/96/eb/7450bc75c31d5be5f7a6d02d26beef6989a4ca6f5efdec65eea6cf612d0e/psycopg_binary-3.3.2-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d391b70c9cc23f6e1142729772a011f364199d2c5ddc0d596f5f43316fbf982d", size = 4991626, upload-time = "2025-12-06T17:32:41.373Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/dc/85/65f14453804c82a7fba31cd1a984b90349c0f327b809102c4b99115c0930/psycopg_binary-3.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f3f601f32244a677c7b029ec39412db2772ad04a28bc2cbb4b1f0931ed0ffad7", size = 4516760, upload-time = "2025-12-06T17:32:44.921Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/24/8c/3105f00a91d73d9a443932f95156eae8159d5d9cb68a9d2cf512710d484f/psycopg_binary-3.3.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:0ae60e910531cfcc364a8f615a7941cac89efeb3f0fffe0c4824a6d11461eef7", size = 4204028, upload-time = "2025-12-06T17:32:48.355Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/1e/dd/74f64a383342ef7c22d1eb2768ed86411c7f877ed2580cd33c17f436fe3c/psycopg_binary-3.3.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7c43a773dd1a481dbb2fe64576aa303d80f328cce0eae5e3e4894947c41d1da7", size = 3935780, upload-time = "2025-12-06T17:32:51.347Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/85/30/f3f207d1c292949a26cdea6727c9c325b4ee41e04bf2736a4afbe45eb61f/psycopg_binary-3.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5a327327f1188b3fbecac41bf1973a60b86b2eb237db10dc945bd3dc97ec39e4", size = 4243239, upload-time = "2025-12-06T17:32:54.924Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b3/08/8f1b5d6231338bf7bc46f635c4d4965facec52e1c9a7952ca8a70cb57dc0/psycopg_binary-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:136c43f185244893a527540307167f5d3ef4e08786508afe45d6f146228f5aa9", size = 3548102, upload-time = "2025-12-06T17:32:57.944Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/4e/1e/8614b01c549dd7e385dacdcd83fe194f6b3acb255a53cc67154ee6bf00e7/psycopg_binary-3.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a9387ab615f929e71ef0f4a8a51e986fa06236ccfa9f3ec98a88f60fbf230634", size = 4579832, upload-time = "2025-12-06T17:33:01.388Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/26/97/0bb093570fae2f4454d42c1ae6000f15934391867402f680254e4a7def54/psycopg_binary-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3ff7489df5e06c12d1829544eaec64970fe27fe300f7cf04c8495fe682064688", size = 4658786, upload-time = "2025-12-06T17:33:05.022Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/61/20/1d9383e3f2038826900a14137b0647d755f67551aab316e1021443105ed5/psycopg_binary-3.3.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:9742580ecc8e1ac45164e98d32ca6df90da509c2d3ff26be245d94c430f92db4", size = 5454896, upload-time = "2025-12-06T17:33:09.023Z" }, @@ -2257,27 +1365,6 @@ wheels = [ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/72/f7/212343c1c9cfac35fd943c527af85e9091d633176e2a407a0797856ff7b9/psycopg_binary-3.3.2-cp314-cp314-win_amd64.whl", hash = "sha256:04bb2de4ba69d6f8395b446ede795e8884c040ec71d01dd07ac2b2d18d4153d1", size = 3642122, upload-time = "2025-12-06T17:34:52.506Z" }, ] -[[package]] -name = "py-key-value-aio" -version = "0.4.4" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } -dependencies = [ - { name = "beartype" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/04/3c/0397c072a38d4bc580994b42e0c90c5f44f679303489e4376289534735e5/py_key_value_aio-0.4.4.tar.gz", hash = "sha256:e3012e6243ed7cc09bb05457bd4d03b1ba5c2b1ca8700096b3927db79ffbbe55", size = 92300, upload-time = "2026-02-16T21:21:43.245Z" } -wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/32/69/f1b537ee70b7def42d63124a539ed3026a11a3ffc3086947a1ca6e861868/py_key_value_aio-0.4.4-py3-none-any.whl", hash = "sha256:18e17564ecae61b987f909fc2cd41ee2012c84b4b1dcb8c055cf8b4bc1bf3f5d", size = 152291, upload-time = "2026-02-16T21:21:44.241Z" }, -] - -[package.optional-dependencies] -memory = [ - { name = "cachetools" }, -] -redis = [ - { name = "redis" }, -] - [[package]] name = "pycparser" version = "2.23" @@ -2311,33 +1398,6 @@ dependencies = [ ] sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c6/90/32c9941e728d564b411d574d8ee0cf09b12ec978cb22b294995bae5549a5/pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146", size = 2107298, upload-time = "2025-11-04T13:39:04.116Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/fb/a8/61c96a77fe28993d9a6fb0f4127e05430a267b235a124545d79fea46dd65/pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2", size = 1901475, upload-time = "2025-11-04T13:39:06.055Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/5d/b6/338abf60225acc18cdc08b4faef592d0310923d19a87fba1faf05af5346e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97", size = 1918815, upload-time = "2025-11-04T13:39:10.41Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d1/1c/2ed0433e682983d8e8cba9c8d8ef274d4791ec6a6f24c58935b90e780e0a/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9", size = 2065567, upload-time = "2025-11-04T13:39:12.244Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b3/24/cf84974ee7d6eae06b9e63289b7b8f6549d416b5c199ca2d7ce13bbcf619/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52", size = 2230442, upload-time = "2025-11-04T13:39:13.962Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/fd/21/4e287865504b3edc0136c89c9c09431be326168b1eb7841911cbc877a995/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941", size = 2350956, upload-time = "2025-11-04T13:39:15.889Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a8/76/7727ef2ffa4b62fcab916686a68a0426b9b790139720e1934e8ba797e238/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a", size = 2068253, upload-time = "2025-11-04T13:39:17.403Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d5/8c/a4abfc79604bcb4c748e18975c44f94f756f08fb04218d5cb87eb0d3a63e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c", size = 2177050, upload-time = "2025-11-04T13:39:19.351Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/67/b1/de2e9a9a79b480f9cb0b6e8b6ba4c50b18d4e89852426364c66aa82bb7b3/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2", size = 2147178, upload-time = "2025-11-04T13:39:21Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/16/c1/dfb33f837a47b20417500efaa0378adc6635b3c79e8369ff7a03c494b4ac/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556", size = 2341833, upload-time = "2025-11-04T13:39:22.606Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/47/36/00f398642a0f4b815a9a558c4f1dca1b4020a7d49562807d7bc9ff279a6c/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49", size = 2321156, upload-time = "2025-11-04T13:39:25.843Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/7e/70/cad3acd89fde2010807354d978725ae111ddf6d0ea46d1ea1775b5c1bd0c/pydantic_core-2.41.5-cp310-cp310-win32.whl", hash = "sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba", size = 1989378, upload-time = "2025-11-04T13:39:27.92Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/76/92/d338652464c6c367e5608e4488201702cd1cbb0f33f7b6a85a60fe5f3720/pydantic_core-2.41.5-cp310-cp310-win_amd64.whl", hash = "sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9", size = 2013622, upload-time = "2025-11-04T13:39:29.848Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873, upload-time = "2025-11-04T13:39:31.373Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826, upload-time = "2025-11-04T13:39:32.897Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869, upload-time = "2025-11-04T13:39:34.469Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890, upload-time = "2025-11-04T13:39:36.053Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740, upload-time = "2025-11-04T13:39:37.753Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021, upload-time = "2025-11-04T13:39:40.94Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378, upload-time = "2025-11-04T13:39:42.523Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761, upload-time = "2025-11-04T13:39:44.553Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303, upload-time = "2025-11-04T13:39:46.238Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355, upload-time = "2025-11-04T13:39:48.002Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875, upload-time = "2025-11-04T13:39:49.705Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549, upload-time = "2025-11-04T13:39:51.842Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305, upload-time = "2025-11-04T13:39:53.485Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902, upload-time = "2025-11-04T13:39:56.488Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" }, @@ -2394,43 +1454,10 @@ wheels = [ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441, upload-time = "2025-11-04T13:42:39.557Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905, upload-time = "2025-11-04T13:42:47.156Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e6/b0/1a2aa41e3b5a4ba11420aba2d091b2d17959c8d1519ece3627c371951e73/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8", size = 2103351, upload-time = "2025-11-04T13:43:02.058Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a4/ee/31b1f0020baaf6d091c87900ae05c6aeae101fa4e188e1613c80e4f1ea31/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a", size = 1925363, upload-time = "2025-11-04T13:43:05.159Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e1/89/ab8e86208467e467a80deaca4e434adac37b10a9d134cd2f99b28a01e483/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b", size = 2135615, upload-time = "2025-11-04T13:43:08.116Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/99/0a/99a53d06dd0348b2008f2f30884b34719c323f16c3be4e6cc1203b74a91d/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2", size = 2175369, upload-time = "2025-11-04T13:43:12.49Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/6d/94/30ca3b73c6d485b9bb0bc66e611cff4a7138ff9736b7e66bcf0852151636/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093", size = 2144218, upload-time = "2025-11-04T13:43:15.431Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/87/57/31b4f8e12680b739a91f472b5671294236b82586889ef764b5fbc6669238/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a", size = 2329951, upload-time = "2025-11-04T13:43:18.062Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/7d/73/3c2c8edef77b8f7310e6fb012dbc4b8551386ed575b9eb6fb2506e28a7eb/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963", size = 2318428, upload-time = "2025-11-04T13:43:20.679Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/2f/02/8559b1f26ee0d502c74f9cca5c0d2fd97e967e083e006bbbb4e97f3a043a/pydantic_core-2.41.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a", size = 2147009, upload-time = "2025-11-04T13:43:23.286Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980, upload-time = "2025-11-04T13:43:25.97Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865, upload-time = "2025-11-04T13:43:28.763Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256, upload-time = "2025-11-04T13:43:31.71Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762, upload-time = "2025-11-04T13:43:34.744Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141, upload-time = "2025-11-04T13:43:37.701Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317, upload-time = "2025-11-04T13:43:40.406Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992, upload-time = "2025-11-04T13:43:43.602Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" }, -] - -[[package]] -name = "pydantic-extra-types" -version = "2.11.1" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } -dependencies = [ - { name = "pydantic" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/66/71/dba38ee2651f84f7842206adbd2233d8bbdb59fb85e9fa14232486a8c471/pydantic_extra_types-2.11.1.tar.gz", hash = "sha256:46792d2307383859e923d8fcefa82108b1a141f8a9c0198982b3832ab5ef1049", size = 172002, upload-time = "2026-03-16T08:08:03.92Z" } -wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/17/c1/3226e6d7f5a4f736f38ac11a6fbb262d701889802595cdb0f53a885ac2e0/pydantic_extra_types-2.11.1-py3-none-any.whl", hash = "sha256:1722ea2bddae5628ace25f2aa685b69978ef533123e5638cfbddb999e0100ec1", size = 79526, upload-time = "2026-03-16T08:08:02.533Z" }, ] [[package]] @@ -2461,29 +1488,12 @@ wheels = [ ] [[package]] -name = "pydocket" -version = "0.18.2" +name = "pydyf" +version = "0.12.1" source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } -dependencies = [ - { name = "cloudpickle" }, - { name = "cronsim" }, - { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, - { name = "fakeredis", extra = ["lua"] }, - { name = "opentelemetry-api" }, - { name = "prometheus-client" }, - { name = "py-key-value-aio", extra = ["memory", "redis"] }, - { name = "python-json-logger" }, - { name = "redis" }, - { name = "rich" }, - { name = "taskgroup", marker = "python_full_version < '3.11'" }, - { name = "typer" }, - { name = "typing-extensions" }, - { name = "tzdata", marker = "sys_platform == 'win32'" }, - { name = "uncalled-for" }, -] -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b8/5f/82dde9fb6099b960a4203596d3b755d1bd2c0d0210fea104d015d6515d7f/pydocket-0.18.2.tar.gz", hash = "sha256:cc2051d15557f83bb164a83b0743fa9c12c2bfe9a9145cff3a5922b4935ce4f5", size = 354762, upload-time = "2026-03-10T13:09:22.52Z" } +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/36/ee/fb410c5c854b6a081a49077912a9765aeffd8e07cbb0663cfda310b01fb4/pydyf-0.12.1.tar.gz", hash = "sha256:fbd7e759541ac725c29c506612003de393249b94310ea78ae44cb1d04b220095", size = 17716, upload-time = "2025-12-02T14:52:14.244Z" } wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/4f/cf/8c1b6340baf81d7f6c97fe0181bda7cfd500d5e33bf469fbffbdae07b3c9/pydocket-0.18.2-py3-none-any.whl", hash = "sha256:19e48de15e83370f750e362610b777533ff9c0fa48bf36766ed581f91d266556", size = 99041, upload-time = "2026-03-10T13:09:20.598Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/22/11/47efe2f66ba848a107adfd490b508f5c0cedc82127950553dca44d29e6c4/pydyf-0.12.1-py3-none-any.whl", hash = "sha256:ea25b4e1fe7911195cb57067560daaa266639184e8335365cc3ee5214e7eaadc", size = 8028, upload-time = "2025-12-02T14:52:12.938Z" }, ] [[package]] @@ -2513,18 +1523,25 @@ wheels = [ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/7c/4c/ad33b92b9864cbde84f259d5df035a6447f91891f5be77788e2a3892bce3/pymysql-1.1.2-py3-none-any.whl", hash = "sha256:e6b1d89711dd51f8f74b1631fe08f039e7d76cf67a42a323d3178f0f25762ed9", size = 45300, upload-time = "2025-08-24T12:55:53.394Z" }, ] +[[package]] +name = "pyphen" +version = "0.17.2" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/69/56/e4d7e1bd70d997713649c5ce530b2d15a5fc2245a74ca820fc2d51d89d4d/pyphen-0.17.2.tar.gz", hash = "sha256:f60647a9c9b30ec6c59910097af82bc5dd2d36576b918e44148d8b07ef3b4aa3", size = 2079470, upload-time = "2025-01-20T13:18:36.296Z" } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/7b/1f/c2142d2edf833a90728e5cdeb10bdbdc094dde8dbac078cee0cf33f5e11b/pyphen-0.17.2-py3-none-any.whl", hash = "sha256:3a07fb017cb2341e1d9ff31b8634efb1ae4dc4b130468c7c39dd3d32e7c3affd", size = 2079358, upload-time = "2025-01-20T13:18:29.629Z" }, +] + [[package]] name = "pytest" version = "9.0.2" source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "iniconfig" }, { name = "packaging" }, { name = "pluggy" }, { name = "pygments" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } wheels = [ @@ -2536,7 +1553,6 @@ name = "pytest-asyncio" version = "1.4.0" source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } dependencies = [ - { name = "backports-asyncio-runner", marker = "python_full_version < '3.11'" }, { name = "pytest" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] @@ -2566,15 +1582,6 @@ wheels = [ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" }, ] -[[package]] -name = "python-json-logger" -version = "4.0.0" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/29/bf/eca6a3d43db1dae7070f70e160ab20b807627ba953663ba07928cdd3dc58/python_json_logger-4.0.0.tar.gz", hash = "sha256:f58e68eb46e1faed27e0f574a55a0455eecd7b8a5b88b85a784519ba3cff047f", size = 17683, upload-time = "2025-10-06T04:15:18.984Z" } -wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/51/e5/fecf13f06e5e5f67e8837d777d1bc43fac0ed2b77a676804df5c34744727/python_json_logger-4.0.0-py3-none-any.whl", hash = "sha256:af09c9daf6a813aa4cc7180395f50f2a9e5fa056034c9953aec92e381c5ba1e2", size = 15548, upload-time = "2025-10-06T04:15:17.553Z" }, -] - [[package]] name = "python-multipart" version = "0.0.9" @@ -2584,18 +1591,6 @@ wheels = [ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/3d/47/444768600d9e0ebc82f8e347775d24aef8f6348cf00e9fa0e81910814e6d/python_multipart-0.0.9-py3-none-any.whl", hash = "sha256:97ca7b8ea7b05f977dc3849c3ba99d51689822fab725c3703af7c866a0c2b215", size = 22299, upload-time = "2024-02-10T13:32:02.969Z" }, ] -[[package]] -name = "python-slugify" -version = "8.0.4" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } -dependencies = [ - { name = "text-unidecode" }, -] -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/87/c7/5e1547c44e31da50a460df93af11a535ace568ef89d7a811069ead340c4a/python-slugify-8.0.4.tar.gz", hash = "sha256:59202371d1d05b54a9e7720c5e038f928f45daaffe41dd10822f3907b937c856", size = 10921, upload-time = "2024-02-08T18:32:45.488Z" } -wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a4/62/02da182e544a51a5c3ccf4b03ab79df279f9c60c5e82d5e8bec7ca26ac11/python_slugify-8.0.4-py2.py3-none-any.whl", hash = "sha256:276540b79961052b66b7d116620b36518847f52d5fd9e3a70164fc8c50faa6b8", size = 10051, upload-time = "2024-02-08T18:32:43.911Z" }, -] - [[package]] name = "pytz" version = "2025.2" @@ -2605,52 +1600,12 @@ wheels = [ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" }, ] -[[package]] -name = "pywin32" -version = "311" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } -wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/7b/40/44efbb0dfbd33aca6a6483191dae0716070ed99e2ecb0c53683f400a0b4f/pywin32-311-cp310-cp310-win32.whl", hash = "sha256:d03ff496d2a0cd4a5893504789d4a15399133fe82517455e78bad62efbb7f0a3", size = 8760432, upload-time = "2025-07-14T20:13:05.9Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/5e/bf/360243b1e953bd254a82f12653974be395ba880e7ec23e3731d9f73921cc/pywin32-311-cp310-cp310-win_amd64.whl", hash = "sha256:797c2772017851984b97180b0bebe4b620bb86328e8a884bb626156295a63b3b", size = 9590103, upload-time = "2025-07-14T20:13:07.698Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/57/38/d290720e6f138086fb3d5ffe0b6caa019a791dd57866940c82e4eeaf2012/pywin32-311-cp310-cp310-win_arm64.whl", hash = "sha256:0502d1facf1fed4839a9a51ccbcc63d952cf318f78ffc00a7e78528ac27d7a2b", size = 8778557, upload-time = "2025-07-14T20:13:11.11Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/7c/af/449a6a91e5d6db51420875c54f6aff7c97a86a3b13a0b4f1a5c13b988de3/pywin32-311-cp311-cp311-win32.whl", hash = "sha256:184eb5e436dea364dcd3d2316d577d625c0351bf237c4e9a5fabbcfa5a58b151", size = 8697031, upload-time = "2025-07-14T20:13:13.266Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/51/8f/9bb81dd5bb77d22243d33c8397f09377056d5c687aa6d4042bea7fbf8364/pywin32-311-cp311-cp311-win_amd64.whl", hash = "sha256:3ce80b34b22b17ccbd937a6e78e7225d80c52f5ab9940fe0506a1a16f3dab503", size = 9508308, upload-time = "2025-07-14T20:13:15.147Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/44/7b/9c2ab54f74a138c491aba1b1cd0795ba61f144c711daea84a88b63dc0f6c/pywin32-311-cp311-cp311-win_arm64.whl", hash = "sha256:a733f1388e1a842abb67ffa8e7aad0e70ac519e09b0f6a784e65a136ec7cefd2", size = 8703930, upload-time = "2025-07-14T20:13:16.945Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e7/ab/01ea1943d4eba0f850c3c61e78e8dd59757ff815ff3ccd0a84de5f541f42/pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31", size = 8706543, upload-time = "2025-07-14T20:13:20.765Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d1/a8/a0e8d07d4d051ec7502cd58b291ec98dcc0c3fff027caad0470b72cfcc2f/pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067", size = 9495040, upload-time = "2025-07-14T20:13:22.543Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ba/3a/2ae996277b4b50f17d61f0603efd8253cb2d79cc7ae159468007b586396d/pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852", size = 8710102, upload-time = "2025-07-14T20:13:24.682Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700, upload-time = "2025-07-14T20:13:26.471Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700, upload-time = "2025-07-14T20:13:28.243Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318, upload-time = "2025-07-14T20:13:30.348Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c9/31/097f2e132c4f16d99a22bfb777e0fd88bd8e1c634304e102f313af69ace5/pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee", size = 8840714, upload-time = "2025-07-14T20:13:32.449Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/90/4b/07c77d8ba0e01349358082713400435347df8426208171ce297da32c313d/pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87", size = 9656800, upload-time = "2025-07-14T20:13:34.312Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540, upload-time = "2025-07-14T20:13:36.379Z" }, -] - [[package]] name = "pyyaml" version = "6.0.3" source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, @@ -2691,202 +1646,15 @@ wheels = [ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, ] -[[package]] -name = "readchar" -version = "4.2.1" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/dd/f8/8657b8cbb4ebeabfbdf991ac40eca8a1d1bd012011bd44ad1ed10f5cb494/readchar-4.2.1.tar.gz", hash = "sha256:91ce3faf07688de14d800592951e5575e9c7a3213738ed01d394dcc949b79adb", size = 9685, upload-time = "2024-11-04T18:28:07.757Z" } -wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a9/10/e4b1e0e5b6b6745c8098c275b69bc9d73e9542d5c7da4f137542b499ed44/readchar-4.2.1-py3-none-any.whl", hash = "sha256:a769305cd3994bb5fa2764aa4073452dc105a4ec39068ffe6efd3c20c60acc77", size = 9350, upload-time = "2024-11-04T18:28:02.859Z" }, -] - [[package]] name = "redis" version = "7.1.0" source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } -dependencies = [ - { name = "async-timeout", marker = "python_full_version < '3.11.3'" }, -] sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/43/c8/983d5c6579a411d8a99bc5823cc5712768859b5ce2c8afe1a65b37832c81/redis-7.1.0.tar.gz", hash = "sha256:b1cc3cfa5a2cb9c2ab3ba700864fb0ad75617b41f01352ce5779dabf6d5f9c3c", size = 4796669, upload-time = "2025-11-19T15:54:39.961Z" } wheels = [ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/89/f0/8956f8a86b20d7bb9d6ac0187cf4cd54d8065bc9a1a09eb8011d4d326596/redis-7.1.0-py3-none-any.whl", hash = "sha256:23c52b208f92b56103e17c5d06bdc1a6c2c0b3106583985a76a18f83b265de2b", size = 354159, upload-time = "2025-11-19T15:54:38.064Z" }, ] -[[package]] -name = "referencing" -version = "0.37.0" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } -dependencies = [ - { name = "attrs" }, - { name = "rpds-py" }, - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, -] -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } -wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, -] - -[[package]] -name = "regex" -version = "2026.1.15" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/0b/86/07d5056945f9ec4590b518171c4254a5925832eb727b56d3c38a7476f316/regex-2026.1.15.tar.gz", hash = "sha256:164759aa25575cbc0651bef59a0b18353e54300d79ace8084c818ad8ac72b7d5", size = 414811, upload-time = "2026-01-14T23:18:02.775Z" } -wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ea/d2/e6ee96b7dff201a83f650241c52db8e5bd080967cb93211f57aa448dc9d6/regex-2026.1.15-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4e3dd93c8f9abe8aa4b6c652016da9a3afa190df5ad822907efe6b206c09896e", size = 488166, upload-time = "2026-01-14T23:13:46.408Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/23/8a/819e9ce14c9f87af026d0690901b3931f3101160833e5d4c8061fa3a1b67/regex-2026.1.15-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:97499ff7862e868b1977107873dd1a06e151467129159a6ffd07b66706ba3a9f", size = 290632, upload-time = "2026-01-14T23:13:48.688Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d5/c3/23dfe15af25d1d45b07dfd4caa6003ad710dcdcb4c4b279909bdfe7a2de8/regex-2026.1.15-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0bda75ebcac38d884240914c6c43d8ab5fb82e74cde6da94b43b17c411aa4c2b", size = 288500, upload-time = "2026-01-14T23:13:50.503Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c6/31/1adc33e2f717df30d2f4d973f8776d2ba6ecf939301efab29fca57505c95/regex-2026.1.15-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7dcc02368585334f5bc81fc73a2a6a0bbade60e7d83da21cead622faf408f32c", size = 781670, upload-time = "2026-01-14T23:13:52.453Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/23/ce/21a8a22d13bc4adcb927c27b840c948f15fc973e21ed2346c1bd0eae22dc/regex-2026.1.15-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:693b465171707bbe882a7a05de5e866f33c76aa449750bee94a8d90463533cc9", size = 850820, upload-time = "2026-01-14T23:13:54.894Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/6c/4f/3eeacdf587a4705a44484cd0b30e9230a0e602811fb3e2cc32268c70d509/regex-2026.1.15-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b0d190e6f013ea938623a58706d1469a62103fb2a241ce2873a9906e0386582c", size = 898777, upload-time = "2026-01-14T23:13:56.908Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/79/a9/1898a077e2965c35fc22796488141a22676eed2d73701e37c73ad7c0b459/regex-2026.1.15-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ff818702440a5878a81886f127b80127f5d50563753a28211482867f8318106", size = 791750, upload-time = "2026-01-14T23:13:58.527Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/4c/84/e31f9d149a178889b3817212827f5e0e8c827a049ff31b4b381e76b26e2d/regex-2026.1.15-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f052d1be37ef35a54e394de66136e30fa1191fab64f71fc06ac7bc98c9a84618", size = 782674, upload-time = "2026-01-14T23:13:59.874Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d2/ff/adf60063db24532add6a1676943754a5654dcac8237af024ede38244fd12/regex-2026.1.15-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6bfc31a37fd1592f0c4fc4bfc674b5c42e52efe45b4b7a6a14f334cca4bcebe4", size = 767906, upload-time = "2026-01-14T23:14:01.298Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/af/3e/e6a216cee1e2780fec11afe7fc47b6f3925d7264e8149c607ac389fd9b1a/regex-2026.1.15-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3d6ce5ae80066b319ae3bc62fd55a557c9491baa5efd0d355f0de08c4ba54e79", size = 774798, upload-time = "2026-01-14T23:14:02.715Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/0f/98/23a4a8378a9208514ed3efc7e7850c27fa01e00ed8557c958df0335edc4a/regex-2026.1.15-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:1704d204bd42b6bb80167df0e4554f35c255b579ba99616def38f69e14a5ccb9", size = 845861, upload-time = "2026-01-14T23:14:04.824Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f8/57/d7605a9d53bd07421a8785d349cd29677fe660e13674fa4c6cbd624ae354/regex-2026.1.15-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:e3174a5ed4171570dc8318afada56373aa9289eb6dc0d96cceb48e7358b0e220", size = 755648, upload-time = "2026-01-14T23:14:06.371Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/6f/76/6f2e24aa192da1e299cc1101674a60579d3912391867ce0b946ba83e2194/regex-2026.1.15-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:87adf5bd6d72e3e17c9cb59ac4096b1faaf84b7eb3037a5ffa61c4b4370f0f13", size = 836250, upload-time = "2026-01-14T23:14:08.343Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/11/3a/1f2a1d29453299a7858eab7759045fc3d9d1b429b088dec2dc85b6fa16a2/regex-2026.1.15-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e85dc94595f4d766bd7d872a9de5ede1ca8d3063f3bdf1e2c725f5eb411159e3", size = 779919, upload-time = "2026-01-14T23:14:09.954Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c0/67/eab9bc955c9dcc58e9b222c801e39cff7ca0b04261792a2149166ce7e792/regex-2026.1.15-cp310-cp310-win32.whl", hash = "sha256:21ca32c28c30d5d65fc9886ff576fc9b59bbca08933e844fa2363e530f4c8218", size = 265888, upload-time = "2026-01-14T23:14:11.35Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/1d/62/31d16ae24e1f8803bddb0885508acecaec997fcdcde9c243787103119ae4/regex-2026.1.15-cp310-cp310-win_amd64.whl", hash = "sha256:3038a62fc7d6e5547b8915a3d927a0fbeef84cdbe0b1deb8c99bbd4a8961b52a", size = 277830, upload-time = "2026-01-14T23:14:12.908Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e5/36/5d9972bccd6417ecd5a8be319cebfd80b296875e7f116c37fb2a2deecebf/regex-2026.1.15-cp310-cp310-win_arm64.whl", hash = "sha256:505831646c945e3e63552cc1b1b9b514f0e93232972a2d5bedbcc32f15bc82e3", size = 270376, upload-time = "2026-01-14T23:14:14.782Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d0/c9/0c80c96eab96948363d270143138d671d5731c3a692b417629bf3492a9d6/regex-2026.1.15-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1ae6020fb311f68d753b7efa9d4b9a5d47a5d6466ea0d5e3b5a471a960ea6e4a", size = 488168, upload-time = "2026-01-14T23:14:16.129Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/17/f0/271c92f5389a552494c429e5cc38d76d1322eb142fb5db3c8ccc47751468/regex-2026.1.15-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:eddf73f41225942c1f994914742afa53dc0d01a6e20fe14b878a1b1edc74151f", size = 290636, upload-time = "2026-01-14T23:14:17.715Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a0/f9/5f1fd077d106ca5655a0f9ff8f25a1ab55b92128b5713a91ed7134ff688e/regex-2026.1.15-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e8cd52557603f5c66a548f69421310886b28b7066853089e1a71ee710e1cdc1", size = 288496, upload-time = "2026-01-14T23:14:19.326Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b5/e1/8f43b03a4968c748858ec77f746c286d81f896c2e437ccf050ebc5d3128c/regex-2026.1.15-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5170907244b14303edc5978f522f16c974f32d3aa92109fabc2af52411c9433b", size = 793503, upload-time = "2026-01-14T23:14:20.922Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/8d/4e/a39a5e8edc5377a46a7c875c2f9a626ed3338cb3bb06931be461c3e1a34a/regex-2026.1.15-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2748c1ec0663580b4510bd89941a31560b4b439a0b428b49472a3d9944d11cd8", size = 860535, upload-time = "2026-01-14T23:14:22.405Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/dc/1c/9dce667a32a9477f7a2869c1c767dc00727284a9fa3ff5c09a5c6c03575e/regex-2026.1.15-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2f2775843ca49360508d080eaa87f94fa248e2c946bbcd963bb3aae14f333413", size = 907225, upload-time = "2026-01-14T23:14:23.897Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a4/3c/87ca0a02736d16b6262921425e84b48984e77d8e4e572c9072ce96e66c30/regex-2026.1.15-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d9ea2604370efc9a174c1b5dcc81784fb040044232150f7f33756049edfc9026", size = 800526, upload-time = "2026-01-14T23:14:26.039Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/4b/ff/647d5715aeea7c87bdcbd2f578f47b415f55c24e361e639fe8c0cc88878f/regex-2026.1.15-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0dcd31594264029b57bf16f37fd7248a70b3b764ed9e0839a8f271b2d22c0785", size = 773446, upload-time = "2026-01-14T23:14:28.109Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/af/89/bf22cac25cb4ba0fe6bff52ebedbb65b77a179052a9d6037136ae93f42f4/regex-2026.1.15-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c08c1f3e34338256732bd6938747daa3c0d5b251e04b6e43b5813e94d503076e", size = 783051, upload-time = "2026-01-14T23:14:29.929Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/1e/f4/6ed03e71dca6348a5188363a34f5e26ffd5db1404780288ff0d79513bce4/regex-2026.1.15-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e43a55f378df1e7a4fa3547c88d9a5a9b7113f653a66821bcea4718fe6c58763", size = 854485, upload-time = "2026-01-14T23:14:31.366Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d9/9a/8e8560bd78caded8eb137e3e47612430a05b9a772caf60876435192d670a/regex-2026.1.15-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:f82110ab962a541737bd0ce87978d4c658f06e7591ba899192e2712a517badbb", size = 762195, upload-time = "2026-01-14T23:14:32.802Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/38/6b/61fc710f9aa8dfcd764fe27d37edfaa023b1a23305a0d84fccd5adb346ea/regex-2026.1.15-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:27618391db7bdaf87ac6c92b31e8f0dfb83a9de0075855152b720140bda177a2", size = 845986, upload-time = "2026-01-14T23:14:34.898Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/fd/2e/fbee4cb93f9d686901a7ca8d94285b80405e8c34fe4107f63ffcbfb56379/regex-2026.1.15-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bfb0d6be01fbae8d6655c8ca21b3b72458606c4aec9bbc932db758d47aba6db1", size = 788992, upload-time = "2026-01-14T23:14:37.116Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ed/14/3076348f3f586de64b1ab75a3fbabdaab7684af7f308ad43be7ef1849e55/regex-2026.1.15-cp311-cp311-win32.whl", hash = "sha256:b10e42a6de0e32559a92f2f8dc908478cc0fa02838d7dbe764c44dca3fa13569", size = 265893, upload-time = "2026-01-14T23:14:38.426Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/0f/19/772cf8b5fc803f5c89ba85d8b1870a1ca580dc482aa030383a9289c82e44/regex-2026.1.15-cp311-cp311-win_amd64.whl", hash = "sha256:e9bf3f0bbdb56633c07d7116ae60a576f846efdd86a8848f8d62b749e1209ca7", size = 277840, upload-time = "2026-01-14T23:14:39.785Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/78/84/d05f61142709474da3c0853222d91086d3e1372bcdab516c6fd8d80f3297/regex-2026.1.15-cp311-cp311-win_arm64.whl", hash = "sha256:41aef6f953283291c4e4e6850607bd71502be67779586a61472beacb315c97ec", size = 270374, upload-time = "2026-01-14T23:14:41.592Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/92/81/10d8cf43c807d0326efe874c1b79f22bfb0fb226027b0b19ebc26d301408/regex-2026.1.15-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:4c8fcc5793dde01641a35905d6731ee1548f02b956815f8f1cab89e515a5bdf1", size = 489398, upload-time = "2026-01-14T23:14:43.741Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/90/b0/7c2a74e74ef2a7c32de724658a69a862880e3e4155cba992ba04d1c70400/regex-2026.1.15-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bfd876041a956e6a90ad7cdb3f6a630c07d491280bfeed4544053cd434901681", size = 291339, upload-time = "2026-01-14T23:14:45.183Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/19/4d/16d0773d0c818417f4cc20aa0da90064b966d22cd62a8c46765b5bd2d643/regex-2026.1.15-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9250d087bc92b7d4899ccd5539a1b2334e44eee85d848c4c1aef8e221d3f8c8f", size = 289003, upload-time = "2026-01-14T23:14:47.25Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c6/e4/1fc4599450c9f0863d9406e944592d968b8d6dfd0d552a7d569e43bceada/regex-2026.1.15-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c8a154cf6537ebbc110e24dabe53095e714245c272da9c1be05734bdad4a61aa", size = 798656, upload-time = "2026-01-14T23:14:48.77Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b2/e6/59650d73a73fa8a60b3a590545bfcf1172b4384a7df2e7fe7b9aab4e2da9/regex-2026.1.15-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8050ba2e3ea1d8731a549e83c18d2f0999fbc99a5f6bd06b4c91449f55291804", size = 864252, upload-time = "2026-01-14T23:14:50.528Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/6e/ab/1d0f4d50a1638849a97d731364c9a80fa304fec46325e48330c170ee8e80/regex-2026.1.15-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf065240704cb8951cc04972cf107063917022511273e0969bdb34fc173456c", size = 912268, upload-time = "2026-01-14T23:14:52.952Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/dd/df/0d722c030c82faa1d331d1921ee268a4e8fb55ca8b9042c9341c352f17fa/regex-2026.1.15-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c32bef3e7aeee75746748643667668ef941d28b003bfc89994ecf09a10f7a1b5", size = 803589, upload-time = "2026-01-14T23:14:55.182Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/66/23/33289beba7ccb8b805c6610a8913d0131f834928afc555b241caabd422a9/regex-2026.1.15-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d5eaa4a4c5b1906bd0d2508d68927f15b81821f85092e06f1a34a4254b0e1af3", size = 775700, upload-time = "2026-01-14T23:14:56.707Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e7/65/bf3a42fa6897a0d3afa81acb25c42f4b71c274f698ceabd75523259f6688/regex-2026.1.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:86c1077a3cc60d453d4084d5b9649065f3bf1184e22992bd322e1f081d3117fb", size = 787928, upload-time = "2026-01-14T23:14:58.312Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f4/f5/13bf65864fc314f68cdd6d8ca94adcab064d4d39dbd0b10fef29a9da48fc/regex-2026.1.15-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:2b091aefc05c78d286657cd4db95f2e6313375ff65dcf085e42e4c04d9c8d410", size = 858607, upload-time = "2026-01-14T23:15:00.657Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a3/31/040e589834d7a439ee43fb0e1e902bc81bd58a5ba81acffe586bb3321d35/regex-2026.1.15-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:57e7d17f59f9ebfa9667e6e5a1c0127b96b87cb9cede8335482451ed00788ba4", size = 763729, upload-time = "2026-01-14T23:15:02.248Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/9b/84/6921e8129687a427edf25a34a5594b588b6d88f491320b9de5b6339a4fcb/regex-2026.1.15-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:c6c4dcdfff2c08509faa15d36ba7e5ef5fcfab25f1e8f85a0c8f45bc3a30725d", size = 850697, upload-time = "2026-01-14T23:15:03.878Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/8a/87/3d06143d4b128f4229158f2de5de6c8f2485170c7221e61bf381313314b2/regex-2026.1.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cf8ff04c642716a7f2048713ddc6278c5fd41faa3b9cab12607c7abecd012c22", size = 789849, upload-time = "2026-01-14T23:15:06.102Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/77/69/c50a63842b6bd48850ebc7ab22d46e7a2a32d824ad6c605b218441814639/regex-2026.1.15-cp312-cp312-win32.whl", hash = "sha256:82345326b1d8d56afbe41d881fdf62f1926d7264b2fc1537f99ae5da9aad7913", size = 266279, upload-time = "2026-01-14T23:15:07.678Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f2/36/39d0b29d087e2b11fd8191e15e81cce1b635fcc845297c67f11d0d19274d/regex-2026.1.15-cp312-cp312-win_amd64.whl", hash = "sha256:4def140aa6156bc64ee9912383d4038f3fdd18fee03a6f222abd4de6357ce42a", size = 277166, upload-time = "2026-01-14T23:15:09.257Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/28/32/5b8e476a12262748851fa8ab1b0be540360692325975b094e594dfebbb52/regex-2026.1.15-cp312-cp312-win_arm64.whl", hash = "sha256:c6c565d9a6e1a8d783c1948937ffc377dd5771e83bd56de8317c450a954d2056", size = 270415, upload-time = "2026-01-14T23:15:10.743Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f8/2e/6870bb16e982669b674cce3ee9ff2d1d46ab80528ee6bcc20fb2292efb60/regex-2026.1.15-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e69d0deeb977ffe7ed3d2e4439360089f9c3f217ada608f0f88ebd67afb6385e", size = 489164, upload-time = "2026-01-14T23:15:13.962Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/dc/67/9774542e203849b0286badf67199970a44ebdb0cc5fb739f06e47ada72f8/regex-2026.1.15-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3601ffb5375de85a16f407854d11cca8fe3f5febbe3ac78fb2866bb220c74d10", size = 291218, upload-time = "2026-01-14T23:15:15.647Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b2/87/b0cda79f22b8dee05f774922a214da109f9a4c0eca5da2c9d72d77ea062c/regex-2026.1.15-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4c5ef43b5c2d4114eb8ea424bb8c9cec01d5d17f242af88b2448f5ee81caadbc", size = 288895, upload-time = "2026-01-14T23:15:17.788Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/3b/6a/0041f0a2170d32be01ab981d6346c83a8934277d82c780d60b127331f264/regex-2026.1.15-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:968c14d4f03e10b2fd960f1d5168c1f0ac969381d3c1fcc973bc45fb06346599", size = 798680, upload-time = "2026-01-14T23:15:19.342Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/58/de/30e1cfcdbe3e891324aa7568b7c968771f82190df5524fabc1138cb2d45a/regex-2026.1.15-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56a5595d0f892f214609c9f76b41b7428bed439d98dc961efafdd1354d42baae", size = 864210, upload-time = "2026-01-14T23:15:22.005Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/64/44/4db2f5c5ca0ccd40ff052ae7b1e9731352fcdad946c2b812285a7505ca75/regex-2026.1.15-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf650f26087363434c4e560011f8e4e738f6f3e029b85d4904c50135b86cfa5", size = 912358, upload-time = "2026-01-14T23:15:24.569Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/79/b6/e6a5665d43a7c42467138c8a2549be432bad22cbd206f5ec87162de74bd7/regex-2026.1.15-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18388a62989c72ac24de75f1449d0fb0b04dfccd0a1a7c1c43af5eb503d890f6", size = 803583, upload-time = "2026-01-14T23:15:26.526Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e7/53/7cd478222169d85d74d7437e74750005e993f52f335f7c04ff7adfda3310/regex-2026.1.15-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6d220a2517f5893f55daac983bfa9fe998a7dbcaee4f5d27a88500f8b7873788", size = 775782, upload-time = "2026-01-14T23:15:29.352Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ca/b5/75f9a9ee4b03a7c009fe60500fe550b45df94f0955ca29af16333ef557c5/regex-2026.1.15-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c9c08c2fbc6120e70abff5d7f28ffb4d969e14294fb2143b4b5c7d20e46d1714", size = 787978, upload-time = "2026-01-14T23:15:31.295Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/72/b3/79821c826245bbe9ccbb54f6eadb7879c722fd3e0248c17bfc90bf54e123/regex-2026.1.15-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7ef7d5d4bd49ec7364315167a4134a015f61e8266c6d446fc116a9ac4456e10d", size = 858550, upload-time = "2026-01-14T23:15:33.558Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/4a/85/2ab5f77a1c465745bfbfcb3ad63178a58337ae8d5274315e2cc623a822fa/regex-2026.1.15-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:6e42844ad64194fa08d5ccb75fe6a459b9b08e6d7296bd704460168d58a388f3", size = 763747, upload-time = "2026-01-14T23:15:35.206Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/6d/84/c27df502d4bfe2873a3e3a7cf1bdb2b9cc10284d1a44797cf38bed790470/regex-2026.1.15-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:cfecdaa4b19f9ca534746eb3b55a5195d5c95b88cac32a205e981ec0a22b7d31", size = 850615, upload-time = "2026-01-14T23:15:37.523Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/7d/b7/658a9782fb253680aa8ecb5ccbb51f69e088ed48142c46d9f0c99b46c575/regex-2026.1.15-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:08df9722d9b87834a3d701f3fca570b2be115654dbfd30179f30ab2f39d606d3", size = 789951, upload-time = "2026-01-14T23:15:39.582Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/fc/2a/5928af114441e059f15b2f63e188bd00c6529b3051c974ade7444b85fcda/regex-2026.1.15-cp313-cp313-win32.whl", hash = "sha256:d426616dae0967ca225ab12c22274eb816558f2f99ccb4a1d52ca92e8baf180f", size = 266275, upload-time = "2026-01-14T23:15:42.108Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/4f/16/5bfbb89e435897bff28cf0352a992ca719d9e55ebf8b629203c96b6ce4f7/regex-2026.1.15-cp313-cp313-win_amd64.whl", hash = "sha256:febd38857b09867d3ed3f4f1af7d241c5c50362e25ef43034995b77a50df494e", size = 277145, upload-time = "2026-01-14T23:15:44.244Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/56/c1/a09ff7392ef4233296e821aec5f78c51be5e91ffde0d163059e50fd75835/regex-2026.1.15-cp313-cp313-win_arm64.whl", hash = "sha256:8e32f7896f83774f91499d239e24cebfadbc07639c1494bb7213983842348337", size = 270411, upload-time = "2026-01-14T23:15:45.858Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/3c/38/0cfd5a78e5c6db00e6782fdae70458f89850ce95baa5e8694ab91d89744f/regex-2026.1.15-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:ec94c04149b6a7b8120f9f44565722c7ae31b7a6d2275569d2eefa76b83da3be", size = 492068, upload-time = "2026-01-14T23:15:47.616Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/50/72/6c86acff16cb7c959c4355826bbf06aad670682d07c8f3998d9ef4fee7cd/regex-2026.1.15-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:40c86d8046915bb9aeb15d3f3f15b6fd500b8ea4485b30e1bbc799dab3fe29f8", size = 292756, upload-time = "2026-01-14T23:15:49.307Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/4e/58/df7fb69eadfe76526ddfce28abdc0af09ffe65f20c2c90932e89d705153f/regex-2026.1.15-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:726ea4e727aba21643205edad8f2187ec682d3305d790f73b7a51c7587b64bdd", size = 291114, upload-time = "2026-01-14T23:15:51.484Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ed/6c/a4011cd1cf96b90d2cdc7e156f91efbd26531e822a7fbb82a43c1016678e/regex-2026.1.15-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1cb740d044aff31898804e7bf1181cc72c03d11dfd19932b9911ffc19a79070a", size = 807524, upload-time = "2026-01-14T23:15:53.102Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/1d/25/a53ffb73183f69c3e9f4355c4922b76d2840aee160af6af5fac229b6201d/regex-2026.1.15-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:05d75a668e9ea16f832390d22131fe1e8acc8389a694c8febc3e340b0f810b93", size = 873455, upload-time = "2026-01-14T23:15:54.956Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/66/0b/8b47fc2e8f97d9b4a851736f3890a5f786443aa8901061c55f24c955f45b/regex-2026.1.15-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d991483606f3dbec93287b9f35596f41aa2e92b7c2ebbb935b63f409e243c9af", size = 915007, upload-time = "2026-01-14T23:15:57.041Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c2/fa/97de0d681e6d26fabe71968dbee06dd52819e9a22fdce5dac7256c31ed84/regex-2026.1.15-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:194312a14819d3e44628a44ed6fea6898fdbecb0550089d84c403475138d0a09", size = 812794, upload-time = "2026-01-14T23:15:58.916Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/22/38/e752f94e860d429654aa2b1c51880bff8dfe8f084268258adf9151cf1f53/regex-2026.1.15-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fe2fda4110a3d0bc163c2e0664be44657431440722c5c5315c65155cab92f9e5", size = 781159, upload-time = "2026-01-14T23:16:00.817Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e9/a7/d739ffaef33c378fc888302a018d7f81080393d96c476b058b8c64fd2b0d/regex-2026.1.15-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:124dc36c85d34ef2d9164da41a53c1c8c122cfb1f6e1ec377a1f27ee81deb794", size = 795558, upload-time = "2026-01-14T23:16:03.267Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/3e/c4/542876f9a0ac576100fc73e9c75b779f5c31e3527576cfc9cb3009dcc58a/regex-2026.1.15-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:a1774cd1981cd212506a23a14dba7fdeaee259f5deba2df6229966d9911e767a", size = 868427, upload-time = "2026-01-14T23:16:05.646Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/fc/0f/d5655bea5b22069e32ae85a947aa564912f23758e112cdb74212848a1a1b/regex-2026.1.15-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:b5f7d8d2867152cdb625e72a530d2ccb48a3d199159144cbdd63870882fb6f80", size = 769939, upload-time = "2026-01-14T23:16:07.542Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/20/06/7e18a4fa9d326daeda46d471a44ef94201c46eaa26dbbb780b5d92cbfdda/regex-2026.1.15-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:492534a0ab925d1db998defc3c302dae3616a2fc3fe2e08db1472348f096ddf2", size = 854753, upload-time = "2026-01-14T23:16:10.395Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/3b/67/dc8946ef3965e166f558ef3b47f492bc364e96a265eb4a2bb3ca765c8e46/regex-2026.1.15-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c661fc820cfb33e166bf2450d3dadbda47c8d8981898adb9b6fe24e5e582ba60", size = 799559, upload-time = "2026-01-14T23:16:12.347Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a5/61/1bba81ff6d50c86c65d9fd84ce9699dd106438ee4cdb105bf60374ee8412/regex-2026.1.15-cp313-cp313t-win32.whl", hash = "sha256:99ad739c3686085e614bf77a508e26954ff1b8f14da0e3765ff7abbf7799f952", size = 268879, upload-time = "2026-01-14T23:16:14.049Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e9/5e/cef7d4c5fb0ea3ac5c775fd37db5747f7378b29526cc83f572198924ff47/regex-2026.1.15-cp313-cp313t-win_amd64.whl", hash = "sha256:32655d17905e7ff8ba5c764c43cb124e34a9245e45b83c22e81041e1071aee10", size = 280317, upload-time = "2026-01-14T23:16:15.718Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b4/52/4317f7a5988544e34ab57b4bde0f04944c4786128c933fb09825924d3e82/regex-2026.1.15-cp313-cp313t-win_arm64.whl", hash = "sha256:b2a13dd6a95e95a489ca242319d18fc02e07ceb28fa9ad146385194d95b3c829", size = 271551, upload-time = "2026-01-14T23:16:17.533Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/52/0a/47fa888ec7cbbc7d62c5f2a6a888878e76169170ead271a35239edd8f0e8/regex-2026.1.15-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:d920392a6b1f353f4aa54328c867fec3320fa50657e25f64abf17af054fc97ac", size = 489170, upload-time = "2026-01-14T23:16:19.835Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ac/c4/d000e9b7296c15737c9301708e9e7fbdea009f8e93541b6b43bdb8219646/regex-2026.1.15-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b5a28980a926fa810dbbed059547b02783952e2efd9c636412345232ddb87ff6", size = 291146, upload-time = "2026-01-14T23:16:21.541Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f9/b6/921cc61982e538682bdf3bdf5b2c6ab6b34368da1f8e98a6c1ddc503c9cf/regex-2026.1.15-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:621f73a07595d83f28952d7bd1e91e9d1ed7625fb7af0064d3516674ec93a2a2", size = 288986, upload-time = "2026-01-14T23:16:23.381Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ca/33/eb7383dde0bbc93f4fb9d03453aab97e18ad4024ac7e26cef8d1f0a2cff0/regex-2026.1.15-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3d7d92495f47567a9b1669c51fc8d6d809821849063d168121ef801bbc213846", size = 799098, upload-time = "2026-01-14T23:16:25.088Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/27/56/b664dccae898fc8d8b4c23accd853f723bde0f026c747b6f6262b688029c/regex-2026.1.15-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8dd16fba2758db7a3780a051f245539c4451ca20910f5a5e6ea1c08d06d4a76b", size = 864980, upload-time = "2026-01-14T23:16:27.297Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/16/40/0999e064a170eddd237bae9ccfcd8f28b3aa98a38bf727a086425542a4fc/regex-2026.1.15-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1e1808471fbe44c1a63e5f577a1d5f02fe5d66031dcbdf12f093ffc1305a858e", size = 911607, upload-time = "2026-01-14T23:16:29.235Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/07/78/c77f644b68ab054e5a674fb4da40ff7bffb2c88df58afa82dbf86573092d/regex-2026.1.15-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0751a26ad39d4f2ade8fe16c59b2bf5cb19eb3d2cd543e709e583d559bd9efde", size = 803358, upload-time = "2026-01-14T23:16:31.369Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/27/31/d4292ea8566eaa551fafc07797961c5963cf5235c797cc2ae19b85dfd04d/regex-2026.1.15-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0f0c7684c7f9ca241344ff95a1de964f257a5251968484270e91c25a755532c5", size = 775833, upload-time = "2026-01-14T23:16:33.141Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ce/b2/cff3bf2fea4133aa6fb0d1e370b37544d18c8350a2fa118c7e11d1db0e14/regex-2026.1.15-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:74f45d170a21df41508cb67165456538425185baaf686281fa210d7e729abc34", size = 788045, upload-time = "2026-01-14T23:16:35.005Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/8d/99/2cb9b69045372ec877b6f5124bda4eb4253bc58b8fe5848c973f752bc52c/regex-2026.1.15-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f1862739a1ffb50615c0fde6bae6569b5efbe08d98e59ce009f68a336f64da75", size = 859374, upload-time = "2026-01-14T23:16:36.919Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/09/16/710b0a5abe8e077b1729a562d2f297224ad079f3a66dce46844c193416c8/regex-2026.1.15-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:453078802f1b9e2b7303fb79222c054cb18e76f7bdc220f7530fdc85d319f99e", size = 763940, upload-time = "2026-01-14T23:16:38.685Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/dd/d1/7585c8e744e40eb3d32f119191969b91de04c073fca98ec14299041f6e7e/regex-2026.1.15-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:a30a68e89e5a218b8b23a52292924c1f4b245cb0c68d1cce9aec9bbda6e2c160", size = 850112, upload-time = "2026-01-14T23:16:40.646Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/af/d6/43e1dd85df86c49a347aa57c1f69d12c652c7b60e37ec162e3096194a278/regex-2026.1.15-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9479cae874c81bf610d72b85bb681a94c95722c127b55445285fb0e2c82db8e1", size = 789586, upload-time = "2026-01-14T23:16:42.799Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/93/38/77142422f631e013f316aaae83234c629555729a9fbc952b8a63ac91462a/regex-2026.1.15-cp314-cp314-win32.whl", hash = "sha256:d639a750223132afbfb8f429c60d9d318aeba03281a5f1ab49f877456448dcf1", size = 271691, upload-time = "2026-01-14T23:16:44.671Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/4a/a9/ab16b4649524ca9e05213c1cdbb7faa85cc2aa90a0230d2f796cbaf22736/regex-2026.1.15-cp314-cp314-win_amd64.whl", hash = "sha256:4161d87f85fa831e31469bfd82c186923070fc970b9de75339b68f0c75b51903", size = 280422, upload-time = "2026-01-14T23:16:46.607Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/be/2a/20fd057bf3521cb4791f69f869635f73e0aaf2b9ad2d260f728144f9047c/regex-2026.1.15-cp314-cp314-win_arm64.whl", hash = "sha256:91c5036ebb62663a6b3999bdd2e559fd8456d17e2b485bf509784cd31a8b1705", size = 273467, upload-time = "2026-01-14T23:16:48.967Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ad/77/0b1e81857060b92b9cad239104c46507dd481b3ff1fa79f8e7f865aae38a/regex-2026.1.15-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ee6854c9000a10938c79238de2379bea30c82e4925a371711af45387df35cab8", size = 492073, upload-time = "2026-01-14T23:16:51.154Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/70/f3/f8302b0c208b22c1e4f423147e1913fd475ddd6230565b299925353de644/regex-2026.1.15-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2c2b80399a422348ce5de4fe40c418d6299a0fa2803dd61dc0b1a2f28e280fcf", size = 292757, upload-time = "2026-01-14T23:16:53.08Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/bf/f0/ef55de2460f3b4a6da9d9e7daacd0cb79d4ef75c64a2af316e68447f0df0/regex-2026.1.15-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:dca3582bca82596609959ac39e12b7dad98385b4fefccb1151b937383cec547d", size = 291122, upload-time = "2026-01-14T23:16:55.383Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/cf/55/bb8ccbacabbc3a11d863ee62a9f18b160a83084ea95cdfc5d207bfc3dd75/regex-2026.1.15-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef71d476caa6692eea743ae5ea23cde3260677f70122c4d258ca952e5c2d4e84", size = 807761, upload-time = "2026-01-14T23:16:57.251Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/8f/84/f75d937f17f81e55679a0509e86176e29caa7298c38bd1db7ce9c0bf6075/regex-2026.1.15-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c243da3436354f4af6c3058a3f81a97d47ea52c9bd874b52fd30274853a1d5df", size = 873538, upload-time = "2026-01-14T23:16:59.349Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b8/d9/0da86327df70349aa8d86390da91171bd3ca4f0e7c1d1d453a9c10344da3/regex-2026.1.15-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8355ad842a7c7e9e5e55653eade3b7d1885ba86f124dd8ab1f722f9be6627434", size = 915066, upload-time = "2026-01-14T23:17:01.607Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/2a/5e/f660fb23fc77baa2a61aa1f1fe3a4eea2bbb8a286ddec148030672e18834/regex-2026.1.15-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f192a831d9575271a22d804ff1a5355355723f94f31d9eef25f0d45a152fdc1a", size = 812938, upload-time = "2026-01-14T23:17:04.366Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/69/33/a47a29bfecebbbfd1e5cd3f26b28020a97e4820f1c5148e66e3b7d4b4992/regex-2026.1.15-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:166551807ec20d47ceaeec380081f843e88c8949780cd42c40f18d16168bed10", size = 781314, upload-time = "2026-01-14T23:17:06.378Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/65/ec/7ec2bbfd4c3f4e494a24dec4c6943a668e2030426b1b8b949a6462d2c17b/regex-2026.1.15-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f9ca1cbdc0fbfe5e6e6f8221ef2309988db5bcede52443aeaee9a4ad555e0dac", size = 795652, upload-time = "2026-01-14T23:17:08.521Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/46/79/a5d8651ae131fe27d7c521ad300aa7f1c7be1dbeee4d446498af5411b8a9/regex-2026.1.15-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b30bcbd1e1221783c721483953d9e4f3ab9c5d165aa709693d3f3946747b1aea", size = 868550, upload-time = "2026-01-14T23:17:10.573Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/06/b7/25635d2809664b79f183070786a5552dd4e627e5aedb0065f4e3cf8ee37d/regex-2026.1.15-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:2a8d7b50c34578d0d3bf7ad58cde9652b7d683691876f83aedc002862a35dc5e", size = 769981, upload-time = "2026-01-14T23:17:12.871Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/16/8b/fc3fcbb2393dcfa4a6c5ffad92dc498e842df4581ea9d14309fcd3c55fb9/regex-2026.1.15-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9d787e3310c6a6425eb346be4ff2ccf6eece63017916fd77fe8328c57be83521", size = 854780, upload-time = "2026-01-14T23:17:14.837Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d0/38/dde117c76c624713c8a2842530be9c93ca8b606c0f6102d86e8cd1ce8bea/regex-2026.1.15-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:619843841e220adca114118533a574a9cd183ed8a28b85627d2844c500a2b0db", size = 799778, upload-time = "2026-01-14T23:17:17.369Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e3/0d/3a6cfa9ae99606afb612d8fb7a66b245a9d5ff0f29bb347c8a30b6ad561b/regex-2026.1.15-cp314-cp314t-win32.whl", hash = "sha256:e90b8db97f6f2c97eb045b51a6b2c5ed69cedd8392459e0642d4199b94fabd7e", size = 274667, upload-time = "2026-01-14T23:17:19.301Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/5b/b2/297293bb0742fd06b8d8e2572db41a855cdf1cae0bf009b1cb74fe07e196/regex-2026.1.15-cp314-cp314t-win_amd64.whl", hash = "sha256:5ef19071f4ac9f0834793af85bd04a920b4407715624e40cb7a0631a11137cdf", size = 284386, upload-time = "2026-01-14T23:17:21.231Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/95/e4/a3b9480c78cf8ee86626cb06f8d931d74d775897d44201ccb813097ae697/regex-2026.1.15-cp314-cp314t-win_arm64.whl", hash = "sha256:ca89c5e596fc05b015f27561b3793dc2fa0917ea0d7507eebb448efd35274a70", size = 274837, upload-time = "2026-01-14T23:17:23.146Z" }, -] - -[[package]] -name = "requests" -version = "2.32.3" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } -dependencies = [ - { name = "certifi" }, - { name = "charset-normalizer" }, - { name = "idna" }, - { name = "urllib3" }, -] -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218, upload-time = "2024-05-29T15:37:49.536Z" } -wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928, upload-time = "2024-05-29T15:37:47.027Z" }, -] - -[[package]] -name = "requests-oauthlib" -version = "2.0.0" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } -dependencies = [ - { name = "oauthlib" }, - { name = "requests" }, -] -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/42/f2/05f29bc3913aea15eb670be136045bf5c5bbf4b99ecb839da9b422bb2c85/requests-oauthlib-2.0.0.tar.gz", hash = "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9", size = 55650, upload-time = "2024-03-22T20:32:29.939Z" } -wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/3b/5d/63d4ae3b9daea098d5d6f5da83984853c1bbacd5dc826764b249fe119d24/requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36", size = 24179, upload-time = "2024-03-22T20:32:28.055Z" }, -] - -[[package]] -name = "rfc3339-validator" -version = "0.1.4" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } -dependencies = [ - { name = "six" }, -] -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/28/ea/a9387748e2d111c3c2b275ba970b735e04e15cdb1eb30693b6b5708c4dbd/rfc3339_validator-0.1.4.tar.gz", hash = "sha256:138a2abdf93304ad60530167e51d2dfb9549521a836871b88d7f4695d0022f6b", size = 5513, upload-time = "2021-05-12T16:37:54.178Z" } -wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/7b/44/4e421b96b67b2daff264473f7465db72fbdf36a07e05494f50300cc7b0c6/rfc3339_validator-0.1.4-py2.py3-none-any.whl", hash = "sha256:24f6ec1eda14ef823da9e36ec7113124b39c04d50a4d3d3a3c2859577e7791fa", size = 3490, upload-time = "2021-05-12T16:37:52.536Z" }, -] - [[package]] name = "rich" version = "13.9.4" @@ -2894,215 +1662,12 @@ source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } dependencies = [ { name = "markdown-it-py" }, { name = "pygments" }, - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ab/3a/0316b28d0761c6734d6bc14e770d85506c986c85ffb239e688eeaab2c2bc/rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098", size = 223149, upload-time = "2024-11-01T16:43:57.873Z" } wheels = [ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/19/71/39c7c0d87f8d4e6c020a393182060eaefeeae6c01dab6a84ec346f2567df/rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90", size = 242424, upload-time = "2024-11-01T16:43:55.817Z" }, ] -[[package]] -name = "rich-rst" -version = "1.3.2" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } -dependencies = [ - { name = "docutils" }, - { name = "rich" }, -] -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/bc/6d/a506aaa4a9eaa945ed8ab2b7347859f53593864289853c5d6d62b77246e0/rich_rst-1.3.2.tar.gz", hash = "sha256:a1196fdddf1e364b02ec68a05e8ff8f6914fee10fbca2e6b6735f166bb0da8d4", size = 14936, upload-time = "2025-10-14T16:49:45.332Z" } -wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/13/2f/b4530fbf948867702d0a3f27de4a6aab1d156f406d72852ab902c4d04de9/rich_rst-1.3.2-py3-none-any.whl", hash = "sha256:a99b4907cbe118cf9d18b0b44de272efa61f15117c61e39ebdc431baf5df722a", size = 12567, upload-time = "2025-10-14T16:49:42.953Z" }, -] - -[[package]] -name = "rpds-py" -version = "0.30.0" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" } -wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/06/0c/0c411a0ec64ccb6d104dcabe0e713e05e153a9a2c3c2bd2b32ce412166fe/rpds_py-0.30.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:679ae98e00c0e8d68a7fda324e16b90fd5260945b45d3b824c892cec9eea3288", size = 370490, upload-time = "2025-11-30T20:21:33.256Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/19/6a/4ba3d0fb7297ebae71171822554abe48d7cab29c28b8f9f2c04b79988c05/rpds_py-0.30.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4cc2206b76b4f576934f0ed374b10d7ca5f457858b157ca52064bdfc26b9fc00", size = 359751, upload-time = "2025-11-30T20:21:34.591Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/cd/7c/e4933565ef7f7a0818985d87c15d9d273f1a649afa6a52ea35ad011195ea/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:389a2d49eded1896c3d48b0136ead37c48e221b391c052fba3f4055c367f60a6", size = 389696, upload-time = "2025-11-30T20:21:36.122Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/5e/01/6271a2511ad0815f00f7ed4390cf2567bec1d4b1da39e2c27a41e6e3b4de/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:32c8528634e1bf7121f3de08fa85b138f4e0dc47657866630611b03967f041d7", size = 403136, upload-time = "2025-11-30T20:21:37.728Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/55/64/c857eb7cd7541e9b4eee9d49c196e833128a55b89a9850a9c9ac33ccf897/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f207f69853edd6f6700b86efb84999651baf3789e78a466431df1331608e5324", size = 524699, upload-time = "2025-11-30T20:21:38.92Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/9c/ed/94816543404078af9ab26159c44f9e98e20fe47e2126d5d32c9d9948d10a/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:67b02ec25ba7a9e8fa74c63b6ca44cf5707f2fbfadae3ee8e7494297d56aa9df", size = 412022, upload-time = "2025-11-30T20:21:40.407Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/61/b5/707f6cf0066a6412aacc11d17920ea2e19e5b2f04081c64526eb35b5c6e7/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0e95f6819a19965ff420f65578bacb0b00f251fefe2c8b23347c37174271f3", size = 390522, upload-time = "2025-11-30T20:21:42.17Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/13/4e/57a85fda37a229ff4226f8cbcf09f2a455d1ed20e802ce5b2b4a7f5ed053/rpds_py-0.30.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:a452763cc5198f2f98898eb98f7569649fe5da666c2dc6b5ddb10fde5a574221", size = 404579, upload-time = "2025-11-30T20:21:43.769Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f9/da/c9339293513ec680a721e0e16bf2bac3db6e5d7e922488de471308349bba/rpds_py-0.30.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e0b65193a413ccc930671c55153a03ee57cecb49e6227204b04fae512eb657a7", size = 421305, upload-time = "2025-11-30T20:21:44.994Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f9/be/522cb84751114f4ad9d822ff5a1aa3c98006341895d5f084779b99596e5c/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:858738e9c32147f78b3ac24dc0edb6610000e56dc0f700fd5f651d0a0f0eb9ff", size = 572503, upload-time = "2025-11-30T20:21:46.91Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a2/9b/de879f7e7ceddc973ea6e4629e9b380213a6938a249e94b0cdbcc325bb66/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:da279aa314f00acbb803da1e76fa18666778e8a8f83484fba94526da5de2cba7", size = 598322, upload-time = "2025-11-30T20:21:48.709Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/48/ac/f01fc22efec3f37d8a914fc1b2fb9bcafd56a299edbe96406f3053edea5a/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7c64d38fb49b6cdeda16ab49e35fe0da2e1e9b34bc38bd78386530f218b37139", size = 560792, upload-time = "2025-11-30T20:21:50.024Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e2/da/4e2b19d0f131f35b6146425f846563d0ce036763e38913d917187307a671/rpds_py-0.30.0-cp310-cp310-win32.whl", hash = "sha256:6de2a32a1665b93233cde140ff8b3467bdb9e2af2b91079f0333a0974d12d464", size = 221901, upload-time = "2025-11-30T20:21:51.32Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/96/cb/156d7a5cf4f78a7cc571465d8aec7a3c447c94f6749c5123f08438bcf7bc/rpds_py-0.30.0-cp310-cp310-win_amd64.whl", hash = "sha256:1726859cd0de969f88dc8673bdd954185b9104e05806be64bcd87badbe313169", size = 235823, upload-time = "2025-11-30T20:21:52.505Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/4d/6e/f964e88b3d2abee2a82c1ac8366da848fce1c6d834dc2132c3fda3970290/rpds_py-0.30.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a2bffea6a4ca9f01b3f8e548302470306689684e61602aa3d141e34da06cf425", size = 370157, upload-time = "2025-11-30T20:21:53.789Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/94/ba/24e5ebb7c1c82e74c4e4f33b2112a5573ddc703915b13a073737b59b86e0/rpds_py-0.30.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc4f992dfe1e2bc3ebc7444f6c7051b4bc13cd8e33e43511e8ffd13bf407010d", size = 359676, upload-time = "2025-11-30T20:21:55.475Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/84/86/04dbba1b087227747d64d80c3b74df946b986c57af0a9f0c98726d4d7a3b/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:422c3cb9856d80b09d30d2eb255d0754b23e090034e1deb4083f8004bd0761e4", size = 389938, upload-time = "2025-11-30T20:21:57.079Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/42/bb/1463f0b1722b7f45431bdd468301991d1328b16cffe0b1c2918eba2c4eee/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07ae8a593e1c3c6b82ca3292efbe73c30b61332fd612e05abee07c79359f292f", size = 402932, upload-time = "2025-11-30T20:21:58.47Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/99/ee/2520700a5c1f2d76631f948b0736cdf9b0acb25abd0ca8e889b5c62ac2e3/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12f90dd7557b6bd57f40abe7747e81e0c0b119bef015ea7726e69fe550e394a4", size = 525830, upload-time = "2025-11-30T20:21:59.699Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e0/ad/bd0331f740f5705cc555a5e17fdf334671262160270962e69a2bdef3bf76/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99b47d6ad9a6da00bec6aabe5a6279ecd3c06a329d4aa4771034a21e335c3a97", size = 412033, upload-time = "2025-11-30T20:22:00.991Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f8/1e/372195d326549bb51f0ba0f2ecb9874579906b97e08880e7a65c3bef1a99/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33f559f3104504506a44bb666b93a33f5d33133765b0c216a5bf2f1e1503af89", size = 390828, upload-time = "2025-11-30T20:22:02.723Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ab/2b/d88bb33294e3e0c76bc8f351a3721212713629ffca1700fa94979cb3eae8/rpds_py-0.30.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:946fe926af6e44f3697abbc305ea168c2c31d3e3ef1058cf68f379bf0335a78d", size = 404683, upload-time = "2025-11-30T20:22:04.367Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/50/32/c759a8d42bcb5289c1fac697cd92f6fe01a018dd937e62ae77e0e7f15702/rpds_py-0.30.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:495aeca4b93d465efde585977365187149e75383ad2684f81519f504f5c13038", size = 421583, upload-time = "2025-11-30T20:22:05.814Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/2b/81/e729761dbd55ddf5d84ec4ff1f47857f4374b0f19bdabfcf929164da3e24/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9a0ca5da0386dee0655b4ccdf46119df60e0f10da268d04fe7cc87886872ba7", size = 572496, upload-time = "2025-11-30T20:22:07.713Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/14/f6/69066a924c3557c9c30baa6ec3a0aa07526305684c6f86c696b08860726c/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8d6d1cc13664ec13c1b84241204ff3b12f9bb82464b8ad6e7a5d3486975c2eed", size = 598669, upload-time = "2025-11-30T20:22:09.312Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/5f/48/905896b1eb8a05630d20333d1d8ffd162394127b74ce0b0784ae04498d32/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3896fa1be39912cf0757753826bc8bdc8ca331a28a7c4ae46b7a21280b06bb85", size = 561011, upload-time = "2025-11-30T20:22:11.309Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/22/16/cd3027c7e279d22e5eb431dd3c0fbc677bed58797fe7581e148f3f68818b/rpds_py-0.30.0-cp311-cp311-win32.whl", hash = "sha256:55f66022632205940f1827effeff17c4fa7ae1953d2b74a8581baaefb7d16f8c", size = 221406, upload-time = "2025-11-30T20:22:13.101Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/fa/5b/e7b7aa136f28462b344e652ee010d4de26ee9fd16f1bfd5811f5153ccf89/rpds_py-0.30.0-cp311-cp311-win_amd64.whl", hash = "sha256:a51033ff701fca756439d641c0ad09a41d9242fa69121c7d8769604a0a629825", size = 236024, upload-time = "2025-11-30T20:22:14.853Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/14/a6/364bba985e4c13658edb156640608f2c9e1d3ea3c81b27aa9d889fff0e31/rpds_py-0.30.0-cp311-cp311-win_arm64.whl", hash = "sha256:47b0ef6231c58f506ef0b74d44e330405caa8428e770fec25329ed2cb971a229", size = 229069, upload-time = "2025-11-30T20:22:16.577Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/03/e7/98a2f4ac921d82f33e03f3835f5bf3a4a40aa1bfdc57975e74a97b2b4bdd/rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad", size = 375086, upload-time = "2025-11-30T20:22:17.93Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/4d/a1/bca7fd3d452b272e13335db8d6b0b3ecde0f90ad6f16f3328c6fb150c889/rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05", size = 359053, upload-time = "2025-11-30T20:22:19.297Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/65/1c/ae157e83a6357eceff62ba7e52113e3ec4834a84cfe07fa4b0757a7d105f/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28", size = 390763, upload-time = "2025-11-30T20:22:21.661Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d4/36/eb2eb8515e2ad24c0bd43c3ee9cd74c33f7ca6430755ccdb240fd3144c44/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd", size = 408951, upload-time = "2025-11-30T20:22:23.408Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d6/65/ad8dc1784a331fabbd740ef6f71ce2198c7ed0890dab595adb9ea2d775a1/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f", size = 514622, upload-time = "2025-11-30T20:22:25.16Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/63/8e/0cfa7ae158e15e143fe03993b5bcd743a59f541f5952e1546b1ac1b5fd45/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1", size = 414492, upload-time = "2025-11-30T20:22:26.505Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/60/1b/6f8f29f3f995c7ffdde46a626ddccd7c63aefc0efae881dc13b6e5d5bb16/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23", size = 394080, upload-time = "2025-11-30T20:22:27.934Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/6d/d5/a266341051a7a3ca2f4b750a3aa4abc986378431fc2da508c5034d081b70/rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6", size = 408680, upload-time = "2025-11-30T20:22:29.341Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/10/3b/71b725851df9ab7a7a4e33cf36d241933da66040d195a84781f49c50490c/rpds_py-0.30.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51", size = 423589, upload-time = "2025-11-30T20:22:31.469Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/00/2b/e59e58c544dc9bd8bd8384ecdb8ea91f6727f0e37a7131baeff8d6f51661/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5", size = 573289, upload-time = "2025-11-30T20:22:32.997Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/da/3e/a18e6f5b460893172a7d6a680e86d3b6bc87a54c1f0b03446a3c8c7b588f/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e", size = 599737, upload-time = "2025-11-30T20:22:34.419Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/5c/e2/714694e4b87b85a18e2c243614974413c60aa107fd815b8cbc42b873d1d7/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394", size = 563120, upload-time = "2025-11-30T20:22:35.903Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/6f/ab/d5d5e3bcedb0a77f4f613706b750e50a5a3ba1c15ccd3665ecc636c968fd/rpds_py-0.30.0-cp312-cp312-win32.whl", hash = "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf", size = 223782, upload-time = "2025-11-30T20:22:37.271Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/39/3b/f786af9957306fdc38a74cef405b7b93180f481fb48453a114bb6465744a/rpds_py-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b", size = 240463, upload-time = "2025-11-30T20:22:39.021Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f3/d2/b91dc748126c1559042cfe41990deb92c4ee3e2b415f6b5234969ffaf0cc/rpds_py-0.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e", size = 230868, upload-time = "2025-11-30T20:22:40.493Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ed/dc/d61221eb88ff410de3c49143407f6f3147acf2538c86f2ab7ce65ae7d5f9/rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2", size = 374887, upload-time = "2025-11-30T20:22:41.812Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/fd/32/55fb50ae104061dbc564ef15cc43c013dc4a9f4527a1f4d99baddf56fe5f/rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8", size = 358904, upload-time = "2025-11-30T20:22:43.479Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/58/70/faed8186300e3b9bdd138d0273109784eea2396c68458ed580f885dfe7ad/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4", size = 389945, upload-time = "2025-11-30T20:22:44.819Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/bd/a8/073cac3ed2c6387df38f71296d002ab43496a96b92c823e76f46b8af0543/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136", size = 407783, upload-time = "2025-11-30T20:22:46.103Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/77/57/5999eb8c58671f1c11eba084115e77a8899d6e694d2a18f69f0ba471ec8b/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7", size = 515021, upload-time = "2025-11-30T20:22:47.458Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e0/af/5ab4833eadc36c0a8ed2bc5c0de0493c04f6c06de223170bd0798ff98ced/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2", size = 414589, upload-time = "2025-11-30T20:22:48.872Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b7/de/f7192e12b21b9e9a68a6d0f249b4af3fdcdff8418be0767a627564afa1f1/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6", size = 394025, upload-time = "2025-11-30T20:22:50.196Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/91/c4/fc70cd0249496493500e7cc2de87504f5aa6509de1e88623431fec76d4b6/rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e", size = 408895, upload-time = "2025-11-30T20:22:51.87Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/58/95/d9275b05ab96556fefff73a385813eb66032e4c99f411d0795372d9abcea/rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d", size = 422799, upload-time = "2025-11-30T20:22:53.341Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/06/c1/3088fc04b6624eb12a57eb814f0d4997a44b0d208d6cace713033ff1a6ba/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7", size = 572731, upload-time = "2025-11-30T20:22:54.778Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d8/42/c612a833183b39774e8ac8fecae81263a68b9583ee343db33ab571a7ce55/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31", size = 599027, upload-time = "2025-11-30T20:22:56.212Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/5f/60/525a50f45b01d70005403ae0e25f43c0384369ad24ffe46e8d9068b50086/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95", size = 563020, upload-time = "2025-11-30T20:22:58.2Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/0b/5d/47c4655e9bcd5ca907148535c10e7d489044243cc9941c16ed7cd53be91d/rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d", size = 223139, upload-time = "2025-11-30T20:23:00.209Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f2/e1/485132437d20aa4d3e1d8b3fb5a5e65aa8139f1e097080c2a8443201742c/rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15", size = 240224, upload-time = "2025-11-30T20:23:02.008Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/24/95/ffd128ed1146a153d928617b0ef673960130be0009c77d8fbf0abe306713/rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1", size = 230645, upload-time = "2025-11-30T20:23:03.43Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ff/1b/b10de890a0def2a319a2626334a7f0ae388215eb60914dbac8a3bae54435/rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a", size = 364443, upload-time = "2025-11-30T20:23:04.878Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/0d/bf/27e39f5971dc4f305a4fb9c672ca06f290f7c4e261c568f3dea16a410d47/rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e", size = 353375, upload-time = "2025-11-30T20:23:06.342Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/40/58/442ada3bba6e8e6615fc00483135c14a7538d2ffac30e2d933ccf6852232/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000", size = 383850, upload-time = "2025-11-30T20:23:07.825Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/14/14/f59b0127409a33c6ef6f5c1ebd5ad8e32d7861c9c7adfa9a624fc3889f6c/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db", size = 392812, upload-time = "2025-11-30T20:23:09.228Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b3/66/e0be3e162ac299b3a22527e8913767d869e6cc75c46bd844aa43fb81ab62/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2", size = 517841, upload-time = "2025-11-30T20:23:11.186Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/3d/55/fa3b9cf31d0c963ecf1ba777f7cf4b2a2c976795ac430d24a1f43d25a6ba/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa", size = 408149, upload-time = "2025-11-30T20:23:12.864Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/60/ca/780cf3b1a32b18c0f05c441958d3758f02544f1d613abf9488cd78876378/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083", size = 383843, upload-time = "2025-11-30T20:23:14.638Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/82/86/d5f2e04f2aa6247c613da0c1dd87fcd08fa17107e858193566048a1e2f0a/rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9", size = 396507, upload-time = "2025-11-30T20:23:16.105Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/4b/9a/453255d2f769fe44e07ea9785c8347edaf867f7026872e76c1ad9f7bed92/rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0", size = 414949, upload-time = "2025-11-30T20:23:17.539Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a3/31/622a86cdc0c45d6df0e9ccb6becdba5074735e7033c20e401a6d9d0e2ca0/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94", size = 565790, upload-time = "2025-11-30T20:23:19.029Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/1c/5d/15bbf0fb4a3f58a3b1c67855ec1efcc4ceaef4e86644665fff03e1b66d8d/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08", size = 590217, upload-time = "2025-11-30T20:23:20.885Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/6d/61/21b8c41f68e60c8cc3b2e25644f0e3681926020f11d06ab0b78e3c6bbff1/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27", size = 555806, upload-time = "2025-11-30T20:23:22.488Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f9/39/7e067bb06c31de48de3eb200f9fc7c58982a4d3db44b07e73963e10d3be9/rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6", size = 211341, upload-time = "2025-11-30T20:23:24.449Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/0a/4d/222ef0b46443cf4cf46764d9c630f3fe4abaa7245be9417e56e9f52b8f65/rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d", size = 225768, upload-time = "2025-11-30T20:23:25.908Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/86/81/dad16382ebbd3d0e0328776d8fd7ca94220e4fa0798d1dc5e7da48cb3201/rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0", size = 362099, upload-time = "2025-11-30T20:23:27.316Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/2b/60/19f7884db5d5603edf3c6bce35408f45ad3e97e10007df0e17dd57af18f8/rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be", size = 353192, upload-time = "2025-11-30T20:23:29.151Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/bf/c4/76eb0e1e72d1a9c4703c69607cec123c29028bff28ce41588792417098ac/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f", size = 384080, upload-time = "2025-11-30T20:23:30.785Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/72/87/87ea665e92f3298d1b26d78814721dc39ed8d2c74b86e83348d6b48a6f31/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f", size = 394841, upload-time = "2025-11-30T20:23:32.209Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/77/ad/7783a89ca0587c15dcbf139b4a8364a872a25f861bdb88ed99f9b0dec985/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87", size = 516670, upload-time = "2025-11-30T20:23:33.742Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/5b/3c/2882bdac942bd2172f3da574eab16f309ae10a3925644e969536553cb4ee/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18", size = 408005, upload-time = "2025-11-30T20:23:35.253Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ce/81/9a91c0111ce1758c92516a3e44776920b579d9a7c09b2b06b642d4de3f0f/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad", size = 382112, upload-time = "2025-11-30T20:23:36.842Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/cf/8e/1da49d4a107027e5fbc64daeab96a0706361a2918da10cb41769244b805d/rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07", size = 399049, upload-time = "2025-11-30T20:23:38.343Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/df/5a/7ee239b1aa48a127570ec03becbb29c9d5a9eb092febbd1699d567cae859/rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f", size = 415661, upload-time = "2025-11-30T20:23:40.263Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/70/ea/caa143cf6b772f823bc7929a45da1fa83569ee49b11d18d0ada7f5ee6fd6/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65", size = 565606, upload-time = "2025-11-30T20:23:42.186Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/64/91/ac20ba2d69303f961ad8cf55bf7dbdb4763f627291ba3d0d7d67333cced9/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f", size = 591126, upload-time = "2025-11-30T20:23:44.086Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/21/20/7ff5f3c8b00c8a95f75985128c26ba44503fb35b8e0259d812766ea966c7/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53", size = 553371, upload-time = "2025-11-30T20:23:46.004Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/72/c7/81dadd7b27c8ee391c132a6b192111ca58d866577ce2d9b0ca157552cce0/rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed", size = 215298, upload-time = "2025-11-30T20:23:47.696Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/3e/d2/1aaac33287e8cfb07aab2e6b8ac1deca62f6f65411344f1433c55e6f3eb8/rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950", size = 228604, upload-time = "2025-11-30T20:23:49.501Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e8/95/ab005315818cc519ad074cb7784dae60d939163108bd2b394e60dc7b5461/rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6", size = 222391, upload-time = "2025-11-30T20:23:50.96Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/9e/68/154fe0194d83b973cdedcdcc88947a2752411165930182ae41d983dcefa6/rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb", size = 364868, upload-time = "2025-11-30T20:23:52.494Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/83/69/8bbc8b07ec854d92a8b75668c24d2abcb1719ebf890f5604c61c9369a16f/rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8", size = 353747, upload-time = "2025-11-30T20:23:54.036Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ab/00/ba2e50183dbd9abcce9497fa5149c62b4ff3e22d338a30d690f9af970561/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7", size = 383795, upload-time = "2025-11-30T20:23:55.556Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/05/6f/86f0272b84926bcb0e4c972262f54223e8ecc556b3224d281e6598fc9268/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898", size = 393330, upload-time = "2025-11-30T20:23:57.033Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/cb/e9/0e02bb2e6dc63d212641da45df2b0bf29699d01715913e0d0f017ee29438/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e", size = 518194, upload-time = "2025-11-30T20:23:58.637Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ee/ca/be7bca14cf21513bdf9c0606aba17d1f389ea2b6987035eb4f62bd923f25/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419", size = 408340, upload-time = "2025-11-30T20:24:00.2Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c2/c7/736e00ebf39ed81d75544c0da6ef7b0998f8201b369acf842f9a90dc8fce/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551", size = 383765, upload-time = "2025-11-30T20:24:01.759Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/4a/3f/da50dfde9956aaf365c4adc9533b100008ed31aea635f2b8d7b627e25b49/rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8", size = 396834, upload-time = "2025-11-30T20:24:03.687Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/4e/00/34bcc2565b6020eab2623349efbdec810676ad571995911f1abdae62a3a0/rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5", size = 415470, upload-time = "2025-11-30T20:24:05.232Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/8c/28/882e72b5b3e6f718d5453bd4d0d9cf8df36fddeb4ddbbab17869d5868616/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404", size = 565630, upload-time = "2025-11-30T20:24:06.878Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/3b/97/04a65539c17692de5b85c6e293520fd01317fd878ea1995f0367d4532fb1/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856", size = 591148, upload-time = "2025-11-30T20:24:08.445Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/85/70/92482ccffb96f5441aab93e26c4d66489eb599efdcf96fad90c14bbfb976/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40", size = 556030, upload-time = "2025-11-30T20:24:10.956Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/20/53/7c7e784abfa500a2b6b583b147ee4bb5a2b3747a9166bab52fec4b5b5e7d/rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0", size = 211570, upload-time = "2025-11-30T20:24:12.735Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", size = 226532, upload-time = "2025-11-30T20:24:14.634Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/69/71/3f34339ee70521864411f8b6992e7ab13ac30d8e4e3309e07c7361767d91/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c2262bdba0ad4fc6fb5545660673925c2d2a5d9e2e0fb603aad545427be0fc58", size = 372292, upload-time = "2025-11-30T20:24:16.537Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/57/09/f183df9b8f2d66720d2ef71075c59f7e1b336bec7ee4c48f0a2b06857653/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ee6af14263f25eedc3bb918a3c04245106a42dfd4f5c2285ea6f997b1fc3f89a", size = 362128, upload-time = "2025-11-30T20:24:18.086Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/7a/68/5c2594e937253457342e078f0cc1ded3dd7b2ad59afdbf2d354869110a02/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3adbb8179ce342d235c31ab8ec511e66c73faa27a47e076ccc92421add53e2bb", size = 391542, upload-time = "2025-11-30T20:24:20.092Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/49/5c/31ef1afd70b4b4fbdb2800249f34c57c64beb687495b10aec0365f53dfc4/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:250fa00e9543ac9b97ac258bd37367ff5256666122c2d0f2bc97577c60a1818c", size = 404004, upload-time = "2025-11-30T20:24:22.231Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e3/63/0cfbea38d05756f3440ce6534d51a491d26176ac045e2707adc99bb6e60a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9854cf4f488b3d57b9aaeb105f06d78e5529d3145b1e4a41750167e8c213c6d3", size = 527063, upload-time = "2025-11-30T20:24:24.302Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/42/e6/01e1f72a2456678b0f618fc9a1a13f882061690893c192fcad9f2926553a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:993914b8e560023bc0a8bf742c5f303551992dcb85e247b1e5c7f4a7d145bda5", size = 413099, upload-time = "2025-11-30T20:24:25.916Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b8/25/8df56677f209003dcbb180765520c544525e3ef21ea72279c98b9aa7c7fb/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58edca431fb9b29950807e301826586e5bbf24163677732429770a697ffe6738", size = 392177, upload-time = "2025-11-30T20:24:27.834Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/4a/b4/0a771378c5f16f8115f796d1f437950158679bcd2a7c68cf251cfb00ed5b/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:dea5b552272a944763b34394d04577cf0f9bd013207bc32323b5a89a53cf9c2f", size = 406015, upload-time = "2025-11-30T20:24:29.457Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/36/d8/456dbba0af75049dc6f63ff295a2f92766b9d521fa00de67a2bd6427d57a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ba3af48635eb83d03f6c9735dfb21785303e73d22ad03d489e88adae6eab8877", size = 423736, upload-time = "2025-11-30T20:24:31.22Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/13/64/b4d76f227d5c45a7e0b796c674fd81b0a6c4fbd48dc29271857d8219571c/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:dff13836529b921e22f15cb099751209a60009731a68519630a24d61f0b1b30a", size = 573981, upload-time = "2025-11-30T20:24:32.934Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/20/91/092bacadeda3edf92bf743cc96a7be133e13a39cdbfd7b5082e7ab638406/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:1b151685b23929ab7beec71080a8889d4d6d9fa9a983d213f07121205d48e2c4", size = 599782, upload-time = "2025-11-30T20:24:35.169Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d1/b7/b95708304cd49b7b6f82fdd039f1748b66ec2b21d6a45180910802f1abf1/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e", size = 562191, upload-time = "2025-11-30T20:24:36.853Z" }, -] - -[[package]] -name = "ruamel-yaml" -version = "0.19.1" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c7/3b/ebda527b56beb90cb7652cb1c7e4f91f48649fbcd8d2eb2fb6e77cd3329b/ruamel_yaml-0.19.1.tar.gz", hash = "sha256:53eb66cd27849eff968ebf8f0bf61f46cdac2da1d1f3576dd4ccee9b25c31993", size = 142709, upload-time = "2026-01-02T16:50:31.84Z" } -wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b8/0c/51f6841f1d84f404f92463fc2b1ba0da357ca1e3db6b7fbda26956c3b82a/ruamel_yaml-0.19.1-py3-none-any.whl", hash = "sha256:27592957fedf6e0b62f281e96effd28043345e0e66001f97683aa9a40c667c93", size = 118102, upload-time = "2026-01-02T16:50:29.201Z" }, -] - -[[package]] -name = "ruamel-yaml-clib" -version = "0.2.15" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ea/97/60fda20e2fb54b83a61ae14648b0817c8f5d84a3821e40bfbdae1437026a/ruamel_yaml_clib-0.2.15.tar.gz", hash = "sha256:46e4cc8c43ef6a94885f72512094e482114a8a706d3c555a34ed4b0d20200600", size = 225794, upload-time = "2025-11-16T16:12:59.761Z" } -wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f7/5a/4ab767cd42dcd65b83c323e1620d7c01ee60a52f4032fb7b61501f45f5c2/ruamel_yaml_clib-0.2.15-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:88eea8baf72f0ccf232c22124d122a7f26e8a24110a0273d9bcddcb0f7e1fa03", size = 147454, upload-time = "2025-11-16T16:13:02.54Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/40/44/184173ac1e74fd35d308108bcbf83904d6ef8439c70763189225a166b238/ruamel_yaml_clib-0.2.15-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9b6f7d74d094d1f3a4e157278da97752f16ee230080ae331fcc219056ca54f77", size = 132467, upload-time = "2025-11-16T16:13:03.539Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/49/1b/2d2077a25fe682ae335007ca831aff42e3cbc93c14066675cf87a6c7fc3e/ruamel_yaml_clib-0.2.15-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4be366220090d7c3424ac2b71c90d1044ea34fca8c0b88f250064fd06087e614", size = 693454, upload-time = "2025-11-16T20:22:41.083Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/90/16/e708059c4c429ad2e33be65507fc1730641e5f239fb2964efc1ba6edea94/ruamel_yaml_clib-0.2.15-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f66f600833af58bea694d5892453f2270695b92200280ee8c625ec5a477eed3", size = 700345, upload-time = "2025-11-16T16:13:04.771Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d9/79/0e8ef51df1f0950300541222e3332f20707a9c210b98f981422937d1278c/ruamel_yaml_clib-0.2.15-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:da3d6adadcf55a93c214d23941aef4abfd45652110aed6580e814152f385b862", size = 731306, upload-time = "2025-11-16T16:13:06.312Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a6/f4/2cdb54b142987ddfbd01fc45ac6bd882695fbcedb9d8bbf796adc3fc3746/ruamel_yaml_clib-0.2.15-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e9fde97ecb7bb9c41261c2ce0da10323e9227555c674989f8d9eb7572fc2098d", size = 692415, upload-time = "2025-11-16T16:13:07.465Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a0/07/40b5fc701cce8240a3e2d26488985d3bbdc446e9fe397c135528d412fea6/ruamel_yaml_clib-0.2.15-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:05c70f7f86be6f7bee53794d80050a28ae7e13e4a0087c1839dcdefd68eb36b6", size = 705007, upload-time = "2025-11-16T20:22:42.856Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/82/19/309258a1df6192fb4a77ffa8eae3e8150e8d0ffa56c1b6fa92e450ba2740/ruamel_yaml_clib-0.2.15-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:6f1d38cbe622039d111b69e9ca945e7e3efebb30ba998867908773183357f3ed", size = 723974, upload-time = "2025-11-16T16:13:08.72Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/67/3a/d6ee8263b521bfceb5cd2faeb904a15936480f2bb01c7ff74a14ec058ca4/ruamel_yaml_clib-0.2.15-cp310-cp310-win32.whl", hash = "sha256:fe239bdfdae2302e93bd6e8264bd9b71290218fff7084a9db250b55caaccf43f", size = 102836, upload-time = "2025-11-16T16:13:10.27Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ed/03/92aeb5c69018387abc49a8bb4f83b54a0471d9ef48e403b24bac68f01381/ruamel_yaml_clib-0.2.15-cp310-cp310-win_amd64.whl", hash = "sha256:468858e5cbde0198337e6a2a78eda8c3fb148bdf4c6498eaf4bc9ba3f8e780bd", size = 121917, upload-time = "2025-11-16T16:13:12.145Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/2c/80/8ce7b9af532aa94dd83360f01ce4716264db73de6bc8efd22c32341f6658/ruamel_yaml_clib-0.2.15-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c583229f336682b7212a43d2fa32c30e643d3076178fb9f7a6a14dde85a2d8bd", size = 147998, upload-time = "2025-11-16T16:13:13.241Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/53/09/de9d3f6b6701ced5f276d082ad0f980edf08ca67114523d1b9264cd5e2e0/ruamel_yaml_clib-0.2.15-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:56ea19c157ed8c74b6be51b5fa1c3aff6e289a041575f0556f66e5fb848bb137", size = 132743, upload-time = "2025-11-16T16:13:14.265Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/0e/f7/73a9b517571e214fe5c246698ff3ed232f1ef863c8ae1667486625ec688a/ruamel_yaml_clib-0.2.15-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5fea0932358e18293407feb921d4f4457db837b67ec1837f87074667449f9401", size = 731459, upload-time = "2025-11-16T20:22:44.338Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/9b/a2/0dc0013169800f1c331a6f55b1282c1f4492a6d32660a0cf7b89e6684919/ruamel_yaml_clib-0.2.15-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef71831bd61fbdb7aa0399d5c4da06bea37107ab5c79ff884cc07f2450910262", size = 749289, upload-time = "2025-11-16T16:13:15.633Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/aa/ed/3fb20a1a96b8dc645d88c4072df481fe06e0289e4d528ebbdcc044ebc8b3/ruamel_yaml_clib-0.2.15-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:617d35dc765715fa86f8c3ccdae1e4229055832c452d4ec20856136acc75053f", size = 777630, upload-time = "2025-11-16T16:13:16.898Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/60/50/6842f4628bc98b7aa4733ab2378346e1441e150935ad3b9f3c3c429d9408/ruamel_yaml_clib-0.2.15-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b45498cc81a4724a2d42273d6cfc243c0547ad7c6b87b4f774cb7bcc131c98d", size = 744368, upload-time = "2025-11-16T16:13:18.117Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d3/b0/128ae8e19a7d794c2e36130a72b3bb650ce1dd13fb7def6cf10656437dcf/ruamel_yaml_clib-0.2.15-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:def5663361f6771b18646620fca12968aae730132e104688766cf8a3b1d65922", size = 745233, upload-time = "2025-11-16T20:22:45.833Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/75/05/91130633602d6ba7ce3e07f8fc865b40d2a09efd4751c740df89eed5caf9/ruamel_yaml_clib-0.2.15-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:014181cdec565c8745b7cbc4de3bf2cc8ced05183d986e6d1200168e5bb59490", size = 770963, upload-time = "2025-11-16T16:13:19.344Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/fd/4b/fd4542e7f33d7d1bc64cc9ac9ba574ce8cf145569d21f5f20133336cdc8c/ruamel_yaml_clib-0.2.15-cp311-cp311-win32.whl", hash = "sha256:d290eda8f6ada19e1771b54e5706b8f9807e6bb08e873900d5ba114ced13e02c", size = 102640, upload-time = "2025-11-16T16:13:20.498Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/bb/eb/00ff6032c19c7537371e3119287999570867a0eafb0154fccc80e74bf57a/ruamel_yaml_clib-0.2.15-cp311-cp311-win_amd64.whl", hash = "sha256:bdc06ad71173b915167702f55d0f3f027fc61abd975bd308a0968c02db4a4c3e", size = 121996, upload-time = "2025-11-16T16:13:21.855Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/72/4b/5fde11a0722d676e469d3d6f78c6a17591b9c7e0072ca359801c4bd17eee/ruamel_yaml_clib-0.2.15-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cb15a2e2a90c8475df45c0949793af1ff413acfb0a716b8b94e488ea95ce7cff", size = 149088, upload-time = "2025-11-16T16:13:22.836Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/85/82/4d08ac65ecf0ef3b046421985e66301a242804eb9a62c93ca3437dc94ee0/ruamel_yaml_clib-0.2.15-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:64da03cbe93c1e91af133f5bec37fd24d0d4ba2418eaf970d7166b0a26a148a2", size = 134553, upload-time = "2025-11-16T16:13:24.151Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b9/cb/22366d68b280e281a932403b76da7a988108287adff2bfa5ce881200107a/ruamel_yaml_clib-0.2.15-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f6d3655e95a80325b84c4e14c080b2470fe4f33b6846f288379ce36154993fb1", size = 737468, upload-time = "2025-11-16T20:22:47.335Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/71/73/81230babf8c9e33770d43ed9056f603f6f5f9665aea4177a2c30ae48e3f3/ruamel_yaml_clib-0.2.15-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:71845d377c7a47afc6592aacfea738cc8a7e876d586dfba814501d8c53c1ba60", size = 753349, upload-time = "2025-11-16T16:13:26.269Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/61/62/150c841f24cda9e30f588ef396ed83f64cfdc13b92d2f925bb96df337ba9/ruamel_yaml_clib-0.2.15-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11e5499db1ccbc7f4b41f0565e4f799d863ea720e01d3e99fa0b7b5fcd7802c9", size = 788211, upload-time = "2025-11-16T16:13:27.441Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/30/93/e79bd9cbecc3267499d9ead919bd61f7ddf55d793fb5ef2b1d7d92444f35/ruamel_yaml_clib-0.2.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4b293a37dc97e2b1e8a1aec62792d1e52027087c8eea4fc7b5abd2bdafdd6642", size = 743203, upload-time = "2025-11-16T16:13:28.671Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/8d/06/1eb640065c3a27ce92d76157f8efddb184bd484ed2639b712396a20d6dce/ruamel_yaml_clib-0.2.15-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:512571ad41bba04eac7268fe33f7f4742210ca26a81fe0c75357fa682636c690", size = 747292, upload-time = "2025-11-16T20:22:48.584Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a5/21/ee353e882350beab65fcc47a91b6bdc512cace4358ee327af2962892ff16/ruamel_yaml_clib-0.2.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e5e9f630c73a490b758bf14d859a39f375e6999aea5ddd2e2e9da89b9953486a", size = 771624, upload-time = "2025-11-16T16:13:29.853Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/57/34/cc1b94057aa867c963ecf9ea92ac59198ec2ee3a8d22a126af0b4d4be712/ruamel_yaml_clib-0.2.15-cp312-cp312-win32.whl", hash = "sha256:f4421ab780c37210a07d138e56dd4b51f8642187cdfb433eb687fe8c11de0144", size = 100342, upload-time = "2025-11-16T16:13:31.067Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b3/e5/8925a4208f131b218f9a7e459c0d6fcac8324ae35da269cb437894576366/ruamel_yaml_clib-0.2.15-cp312-cp312-win_amd64.whl", hash = "sha256:2b216904750889133d9222b7b873c199d48ecbb12912aca78970f84a5aa1a4bc", size = 119013, upload-time = "2025-11-16T16:13:32.164Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/17/5e/2f970ce4c573dc30c2f95825f2691c96d55560268ddc67603dc6ea2dd08e/ruamel_yaml_clib-0.2.15-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4dcec721fddbb62e60c2801ba08c87010bd6b700054a09998c4d09c08147b8fb", size = 147450, upload-time = "2025-11-16T16:13:33.542Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d6/03/a1baa5b94f71383913f21b96172fb3a2eb5576a4637729adbf7cd9f797f8/ruamel_yaml_clib-0.2.15-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:65f48245279f9bb301d1276f9679b82e4c080a1ae25e679f682ac62446fac471", size = 133139, upload-time = "2025-11-16T16:13:34.587Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/dc/19/40d676802390f85784235a05788fd28940923382e3f8b943d25febbb98b7/ruamel_yaml_clib-0.2.15-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:46895c17ead5e22bea5e576f1db7e41cb273e8d062c04a6a49013d9f60996c25", size = 731474, upload-time = "2025-11-16T20:22:49.934Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ce/bb/6ef5abfa43b48dd55c30d53e997f8f978722f02add61efba31380d73e42e/ruamel_yaml_clib-0.2.15-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3eb199178b08956e5be6288ee0b05b2fb0b5c1f309725ad25d9c6ea7e27f962a", size = 748047, upload-time = "2025-11-16T16:13:35.633Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ff/5d/e4f84c9c448613e12bd62e90b23aa127ea4c46b697f3d760acc32cb94f25/ruamel_yaml_clib-0.2.15-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d1032919280ebc04a80e4fb1e93f7a738129857eaec9448310e638c8bccefcf", size = 782129, upload-time = "2025-11-16T16:13:36.781Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/de/4b/e98086e88f76c00c88a6bcf15eae27a1454f661a9eb72b111e6bbb69024d/ruamel_yaml_clib-0.2.15-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ab0df0648d86a7ecbd9c632e8f8d6b21bb21b5fc9d9e095c796cacf32a728d2d", size = 736848, upload-time = "2025-11-16T16:13:37.952Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/0c/5c/5964fcd1fd9acc53b7a3a5d9a05ea4f95ead9495d980003a557deb9769c7/ruamel_yaml_clib-0.2.15-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:331fb180858dd8534f0e61aa243b944f25e73a4dae9962bd44c46d1761126bbf", size = 741630, upload-time = "2025-11-16T20:22:51.718Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/07/1e/99660f5a30fceb58494598e7d15df883a07292346ef5696f0c0ae5dee8c6/ruamel_yaml_clib-0.2.15-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fd4c928ddf6bce586285daa6d90680b9c291cfd045fc40aad34e445d57b1bf51", size = 766619, upload-time = "2025-11-16T16:13:39.178Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/36/2f/fa0344a9327b58b54970e56a27b32416ffbcfe4dcc0700605516708579b2/ruamel_yaml_clib-0.2.15-cp313-cp313-win32.whl", hash = "sha256:bf0846d629e160223805db9fe8cc7aec16aaa11a07310c50c8c7164efa440aec", size = 100171, upload-time = "2025-11-16T16:13:40.456Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/06/c4/c124fbcef0684fcf3c9b72374c2a8c35c94464d8694c50f37eef27f5a145/ruamel_yaml_clib-0.2.15-cp313-cp313-win_amd64.whl", hash = "sha256:45702dfbea1420ba3450bb3dd9a80b33f0badd57539c6aac09f42584303e0db6", size = 118845, upload-time = "2025-11-16T16:13:41.481Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/3e/bd/ab8459c8bb759c14a146990bf07f632c1cbec0910d4853feeee4be2ab8bb/ruamel_yaml_clib-0.2.15-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:753faf20b3a5906faf1fc50e4ddb8c074cb9b251e00b14c18b28492f933ac8ef", size = 147248, upload-time = "2025-11-16T16:13:42.872Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/69/f2/c4cec0a30f1955510fde498aac451d2e52b24afdbcb00204d3a951b772c3/ruamel_yaml_clib-0.2.15-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:480894aee0b29752560a9de46c0e5f84a82602f2bc5c6cde8db9a345319acfdf", size = 133764, upload-time = "2025-11-16T16:13:43.932Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/82/c7/2480d062281385a2ea4f7cc9476712446e0c548cd74090bff92b4b49e898/ruamel_yaml_clib-0.2.15-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4d3b58ab2454b4747442ac76fab66739c72b1e2bb9bd173d7694b9f9dbc9c000", size = 730537, upload-time = "2025-11-16T20:22:52.918Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/75/08/e365ee305367559f57ba6179d836ecc3d31c7d3fdff2a40ebf6c32823a1f/ruamel_yaml_clib-0.2.15-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bfd309b316228acecfa30670c3887dcedf9b7a44ea39e2101e75d2654522acd4", size = 746944, upload-time = "2025-11-16T16:13:45.338Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a1/5c/8b56b08db91e569d0a4fbfa3e492ed2026081bdd7e892f63ba1c88a2f548/ruamel_yaml_clib-0.2.15-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2812ff359ec1f30129b62372e5f22a52936fac13d5d21e70373dbca5d64bb97c", size = 778249, upload-time = "2025-11-16T16:13:46.871Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/6a/1d/70dbda370bd0e1a92942754c873bd28f513da6198127d1736fa98bb2a16f/ruamel_yaml_clib-0.2.15-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7e74ea87307303ba91073b63e67f2c667e93f05a8c63079ee5b7a5c8d0d7b043", size = 737140, upload-time = "2025-11-16T16:13:48.349Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/5b/87/822d95874216922e1120afb9d3fafa795a18fdd0c444f5c4c382f6dac761/ruamel_yaml_clib-0.2.15-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:713cd68af9dfbe0bb588e144a61aad8dcc00ef92a82d2e87183ca662d242f524", size = 741070, upload-time = "2025-11-16T20:22:54.151Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b9/17/4e01a602693b572149f92c983c1f25bd608df02c3f5cf50fd1f94e124a59/ruamel_yaml_clib-0.2.15-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:542d77b72786a35563f97069b9379ce762944e67055bea293480f7734b2c7e5e", size = 765882, upload-time = "2025-11-16T16:13:49.526Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/9f/17/7999399081d39ebb79e807314de6b611e1d1374458924eb2a489c01fc5ad/ruamel_yaml_clib-0.2.15-cp314-cp314-win32.whl", hash = "sha256:424ead8cef3939d690c4b5c85ef5b52155a231ff8b252961b6516ed7cf05f6aa", size = 102567, upload-time = "2025-11-16T16:13:50.78Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d2/67/be582a7370fdc9e6846c5be4888a530dcadd055eef5b932e0e85c33c7d73/ruamel_yaml_clib-0.2.15-cp314-cp314-win_amd64.whl", hash = "sha256:ac9b8d5fa4bb7fd2917ab5027f60d4234345fd366fe39aa711d5dca090aa1467", size = 122847, upload-time = "2025-11-16T16:13:51.807Z" }, -] - [[package]] name = "ruff" version = "0.14.13" @@ -3129,15 +1694,6 @@ wheels = [ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/4d/e1/7348090988095e4e39560cfc2f7555b1b2a7357deba19167b600fdf5215d/ruff-0.14.13-py3-none-win_arm64.whl", hash = "sha256:7ab819e14f1ad9fe39f246cfcc435880ef7a9390d81a2b6ac7e01039083dd247", size = 13080224, upload-time = "2026-01-15T20:14:45.853Z" }, ] -[[package]] -name = "semver" -version = "3.0.4" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/72/d1/d3159231aec234a59dd7d601e9dd9fe96f3afff15efd33c1070019b26132/semver-3.0.4.tar.gz", hash = "sha256:afc7d8c584a5ed0a11033af086e8af226a9c0b206f313e0301f8dd7b6b589602", size = 269730, upload-time = "2025-01-24T13:19:27.617Z" } -wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a6/24/4d91e05817e92e3a61c8a21e08fd0f390f5301f1c448b137c57c4bc6e543/semver-3.0.4-py3-none-any.whl", hash = "sha256:9c824d87ba7f7ab4a1890799cec8596f15c1241cb473404ea1cb0c55e4b04746", size = 17912, upload-time = "2025-01-24T13:19:24.949Z" }, -] - [[package]] name = "shellingham" version = "1.5.4" @@ -3193,20 +1749,6 @@ dependencies = [ ] sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/be/f9/5e4491e5ccf42f5d9cfc663741d261b3e6e1683ae7812114e7636409fcc6/sqlalchemy-2.0.45.tar.gz", hash = "sha256:1632a4bda8d2d25703fdad6363058d882541bdaaee0e5e3ddfa0cd3229efce88", size = 9869912, upload-time = "2025-12-09T21:05:16.737Z" } wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/fe/70/75b1387d72e2847220441166c5eb4e9846dd753895208c13e6d66523b2d9/sqlalchemy-2.0.45-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c64772786d9eee72d4d3784c28f0a636af5b0a29f3fe26ff11f55efe90c0bd85", size = 2154148, upload-time = "2025-12-10T20:03:21.023Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d8/a4/7805e02323c49cb9d1ae5cd4913b28c97103079765f520043f914fca4cb3/sqlalchemy-2.0.45-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7ae64ebf7657395824a19bca98ab10eb9a3ecb026bf09524014f1bb81cb598d4", size = 3233051, upload-time = "2025-12-09T22:06:04.768Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d7/ec/32ae09139f61bef3de3142e85c47abdee8db9a55af2bb438da54a4549263/sqlalchemy-2.0.45-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f02325709d1b1a1489f23a39b318e175a171497374149eae74d612634b234c0", size = 3232781, upload-time = "2025-12-09T22:09:54.435Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ad/bd/bf7b869b6f5585eac34222e1cf4405f4ba8c3b85dd6b1af5d4ce8bca695f/sqlalchemy-2.0.45-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d2c3684fca8a05f0ac1d9a21c1f4a266983a7ea9180efb80ffeb03861ecd01a0", size = 3182096, upload-time = "2025-12-09T22:06:06.169Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/21/6a/c219720a241bb8f35c88815ccc27761f5af7fdef04b987b0e8a2c1a6dcaa/sqlalchemy-2.0.45-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:040f6f0545b3b7da6b9317fc3e922c9a98fc7243b2a1b39f78390fc0942f7826", size = 3205109, upload-time = "2025-12-09T22:09:55.969Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/bd/c4/6ccf31b2bc925d5d95fab403ffd50d20d7c82b858cf1a4855664ca054dce/sqlalchemy-2.0.45-cp310-cp310-win32.whl", hash = "sha256:830d434d609fe7bfa47c425c445a8b37929f140a7a44cdaf77f6d34df3a7296a", size = 2114240, upload-time = "2025-12-09T21:29:54.007Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/de/29/a27a31fca07316def418db6f7c70ab14010506616a2decef1906050a0587/sqlalchemy-2.0.45-cp310-cp310-win_amd64.whl", hash = "sha256:0209d9753671b0da74da2cfbb9ecf9c02f72a759e4b018b3ab35f244c91842c7", size = 2137615, upload-time = "2025-12-09T21:29:55.85Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a2/1c/769552a9d840065137272ebe86ffbb0bc92b0f1e0a68ee5266a225f8cd7b/sqlalchemy-2.0.45-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2e90a344c644a4fa871eb01809c32096487928bd2038bf10f3e4515cb688cc56", size = 2153860, upload-time = "2025-12-10T20:03:23.843Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f3/f8/9be54ff620e5b796ca7b44670ef58bc678095d51b0e89d6e3102ea468216/sqlalchemy-2.0.45-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8c8b41b97fba5f62349aa285654230296829672fc9939cd7f35aab246d1c08b", size = 3309379, upload-time = "2025-12-09T22:06:07.461Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f6/2b/60ce3ee7a5ae172bfcd419ce23259bb874d2cddd44f67c5df3760a1e22f9/sqlalchemy-2.0.45-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:12c694ed6468333a090d2f60950e4250b928f457e4962389553d6ba5fe9951ac", size = 3309948, upload-time = "2025-12-09T22:09:57.643Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a3/42/bac8d393f5db550e4e466d03d16daaafd2bad1f74e48c12673fb499a7fc1/sqlalchemy-2.0.45-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f7d27a1d977a1cfef38a0e2e1ca86f09c4212666ce34e6ae542f3ed0a33bc606", size = 3261239, upload-time = "2025-12-09T22:06:08.879Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/6f/12/43dc70a0528c59842b04ea1c1ed176f072a9b383190eb015384dd102fb19/sqlalchemy-2.0.45-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d62e47f5d8a50099b17e2bfc1b0c7d7ecd8ba6b46b1507b58cc4f05eefc3bb1c", size = 3284065, upload-time = "2025-12-09T22:09:59.454Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/cf/9c/563049cf761d9a2ec7bc489f7879e9d94e7b590496bea5bbee9ed7b4cc32/sqlalchemy-2.0.45-cp311-cp311-win32.whl", hash = "sha256:3c5f76216e7b85770d5bb5130ddd11ee89f4d52b11783674a662c7dd57018177", size = 2113480, upload-time = "2025-12-09T21:29:57.03Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/bc/fa/09d0a11fe9f15c7fa5c7f0dd26be3d235b0c0cbf2f9544f43bc42efc8a24/sqlalchemy-2.0.45-cp311-cp311-win_amd64.whl", hash = "sha256:a15b98adb7f277316f2c276c090259129ee4afca783495e212048daf846654b2", size = 2138407, upload-time = "2025-12-09T21:29:58.556Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/2d/c7/1900b56ce19bff1c26f39a4ce427faec7716c81ac792bfac8b6a9f3dca93/sqlalchemy-2.0.45-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b3ee2aac15169fb0d45822983631466d60b762085bc4535cd39e66bea362df5f", size = 3333760, upload-time = "2025-12-09T22:11:02.66Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/0a/93/3be94d96bb442d0d9a60e55a6bb6e0958dd3457751c6f8502e56ef95fed0/sqlalchemy-2.0.45-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba547ac0b361ab4f1608afbc8432db669bd0819b3e12e29fb5fa9529a8bba81d", size = 3348268, upload-time = "2025-12-09T22:13:49.054Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/48/4b/f88ded696e61513595e4a9778f9d3f2bf7332cce4eb0c7cedaabddd6687b/sqlalchemy-2.0.45-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:215f0528b914e5c75ef2559f69dca86878a3beeb0c1be7279d77f18e8d180ed4", size = 3278144, upload-time = "2025-12-09T22:11:04.14Z" }, @@ -3232,11 +1774,6 @@ wheels = [ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/bf/e1/3ccb13c643399d22289c6a9786c1a91e3dcbb68bce4beb44926ac2c557bf/sqlalchemy-2.0.45-py3-none-any.whl", hash = "sha256:5225a288e4c8cc2308dbdd874edad6e7d0fd38eac1e9e5f23503425c8eee20d0", size = 1936672, upload-time = "2025-12-09T21:54:52.608Z" }, ] -[package.optional-dependencies] -asyncio = [ - { name = "greenlet" }, -] - [[package]] name = "sqlglot" version = "27.8.0" @@ -3257,26 +1794,6 @@ version = "0.6.1" source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/59/13/e77dcfd72b849a113bea7ccee79329f77751704e66560410176b1f4657f9/sqlglotrs-0.6.1.tar.gz", hash = "sha256:f638a7a544698ade8b0c992c8c67feae17bd5c2c760114ab164bd0b7dc8911e1", size = 15420, upload-time = "2025-06-04T11:35:28.831Z" } wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/9f/a9/e20938fed3cca24d234823dcb41791c0d5d3be9c59dd435647e474dcf7d1/sqlglotrs-0.6.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:b8ca06fa7083720138c90d7329037875ab12f067511c060029a664d874989b5d", size = 316594, upload-time = "2025-06-04T11:35:22.263Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f1/6e/0320d82b5471d8e7d554bf92b946f7f0c53729265c500b59c0be770fd25e/sqlglotrs-0.6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8aca21b3b49d34025c709289e970ef51e496cd9e6f0d7437b4c7fbf467a39f36", size = 304428, upload-time = "2025-06-04T11:35:15.63Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/58/82/9e4ae55993fd861209fe85bde6118dcbfd3439708052df568a1c6c5ff5c3/sqlglotrs-0.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e0591fa2608d14999cff6fab232210536c1610b8fa4ac5ed15af48a1f461942", size = 336085, upload-time = "2025-06-04T11:34:27.722Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b2/7f/e550660ad6175f18c4243044399836e7563a5cb38e9a69cfe768b29b6b35/sqlglotrs-0.6.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:621934ec02afe127a84797b319e8e4ee12398e2a99b6def337321294ce686a48", size = 345168, upload-time = "2025-06-04T11:34:36.453Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/22/9f/2b45e5a5459ddab605919b9e1c59ebcc30a47871e152957d3ffaa7d63331/sqlglotrs-0.6.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:72fd1be9b9c37898f647b0ad14a16ceb029c859756ce4395ae8606ed0de1876f", size = 485971, upload-time = "2025-06-04T11:34:53.788Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/3a/06/49cf62fd8d5c344b432c38576b5fc5d9369b2810bc523fa6b2cdd1480032/sqlglotrs-0.6.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7b1e198838b8e95c64289caf2ac44c8874b50889425961a97c605f4bd46b58e9", size = 373694, upload-time = "2025-06-04T11:35:00.861Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b2/7c/ed174731d09e3dc359160326428496c90b24010202a2f2accc43aeda8a8e/sqlglotrs-0.6.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3ae5cbdc1591a26fd67789905222cc4badf023ce6f8ec28c7f00d9cd31a377a", size = 340780, upload-time = "2025-06-04T11:35:08.578Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/76/02/8c253df574ee97f37411f02c4ba50d7811ceae3297c61d094dff9881a382/sqlglotrs-0.6.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e6c541634707956f6b3a4f9384336cf38b8a2b3aa8c02eca591db3feaa3fe133", size = 365650, upload-time = "2025-06-04T11:34:45.618Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/6a/ca/a18f08f423c2c3897fdfe3c4c45c7e73dd6fbec76cc245822afac5b4f76b/sqlglotrs-0.6.1-cp310-cp310-win32.whl", hash = "sha256:f6eef2b117cc35a23d1486351cd3ad341a32a59f9cb043402a9c40b46e135e5d", size = 186681, upload-time = "2025-06-04T11:35:29.587Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e0/9b/114bad2c09288dbb641596bae546ca9ba6b550232d6e280ea1a53a1ac046/sqlglotrs-0.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:eb7e91e61fb3f067d56ade81a4e1b3a8385d730a998086e86f9e02f9d2347b7a", size = 198783, upload-time = "2025-06-04T11:35:36.103Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ae/af/121c2e4190356d0296378677a71d72c406647c5e153bc883a801cca70a01/sqlglotrs-0.6.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:c99b00ab3c88521c4f6431b1bd18bad336b45ec95c2c188da4a59984fdaedffe", size = 316735, upload-time = "2025-06-04T11:35:23.393Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/67/ab/cf64e66de68e7208ebef7bbed1441b2b49ed41f654aad1e3b0f688ec795f/sqlglotrs-0.6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f5c8edf24124f94460f41b68044958cfc0a13ad20f6a148e10e840ebb10fbf2f", size = 304504, upload-time = "2025-06-04T11:35:16.807Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e4/fd/70dcfd20b8ce839180c9be17a06bd46948281f185501bb7f1539f9361412/sqlglotrs-0.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b99c1233eae4c11098fa59009932f2c5e14c06fdc4745bc4004fcf21e0c61eb7", size = 336017, upload-time = "2025-06-04T11:34:29.344Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/1a/6e/6ae6a5c6ac3e2b7c5d24a8fda6171bc60c7d1010e95fac5feed1bf9c6c91/sqlglotrs-0.6.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:79233885ecb12a6c16be87c346954cadd0806e607cd62808d981dc3b029b88b0", size = 345714, upload-time = "2025-06-04T11:34:37.754Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/3d/dd/31e654d760e0b10ed1d15157690131e983b0edf607b6d318006170f251a1/sqlglotrs-0.6.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c340fd06d92f1dd1c6d6ea3926b7c6865af7d018a944622f5715c0622533fc5b", size = 486116, upload-time = "2025-06-04T11:34:54.908Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/48/01/6f4da6389f86a26c715c4e8937e2e6e499394d33db42f87ebf0d87ad18b7/sqlglotrs-0.6.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3898f1b07c9f17a1e3f095a1b6dd008447899a2d636ed4c74a953df45ad6cdca", size = 373777, upload-time = "2025-06-04T11:35:01.95Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/8a/26/a4cad155f33aa96e81b62d02c119ec165d0689fe485cd0d19867d62054a9/sqlglotrs-0.6.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d1ac532bd4f9c76e658542441b0e6ada931856b69d9dbfc076947c6498606dc", size = 340494, upload-time = "2025-06-04T11:35:09.698Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/30/ac/d199a64c155f71fc9db6c400388fb5272479988fcc1b52b292bce3826017/sqlglotrs-0.6.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6fb1a250d1e8066b34d8f900762534d81f1ccc0d41aa157ed6b26e5712834c5d", size = 365531, upload-time = "2025-06-04T11:34:47.022Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/79/71/b16ba44b41c4b9981c177eee39c0092900721465d3439b8cab15ab5b23ac/sqlglotrs-0.6.1-cp311-cp311-win32.whl", hash = "sha256:32617a5ed23703d55c5cc92b02b56269fb8838f6ed5b45d7a4aaba27a4c5a4c8", size = 186529, upload-time = "2025-06-04T11:35:30.625Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/40/63/d6f86a732632dd5773b1b7afbc8be53ba1d96858dd75050c2c59317ee4ed/sqlglotrs-0.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:1fc86e8c9a6d097eedc7d3c7218ea0376793a03a8abedd4dce22001fc314edd1", size = 199329, upload-time = "2025-06-04T11:35:37.294Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/2a/0d/bcd591085619d06037878085f6d96db5e6cfe235ee597bfcb45dfc1686c7/sqlglotrs-0.6.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f07f9dcfafbfcba1f9fa642e6408c3100061378ce998943104af233e995f1c90", size = 312362, upload-time = "2025-06-04T11:35:24.529Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f1/13/b0825b96edc0b7dc41d82e3d9997ee259a152c6800d036aba4bdbc24bdc9/sqlglotrs-0.6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5d4ac1f4b7a1c2f3aea55710af2ed1c14123534cf0678451e39dbc87dfc58a51", size = 300618, upload-time = "2025-06-04T11:35:17.924Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/91/8d/950597492e3c03893f2d3bbb45a423778b1216e1d6ac59a678af1a47cc34/sqlglotrs-0.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c90403de2fdea37163ffecba43f45441ea089e9e12b6859ea7c9b4527a3b1df3", size = 336609, upload-time = "2025-06-04T11:34:30.862Z" }, @@ -3311,40 +1828,6 @@ wheels = [ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/0a/0f/64baf7a06492e8c12f5c4b49db286787a7255195df496fc21f5fd9eecffa/starlette-0.40.0-py3-none-any.whl", hash = "sha256:c494a22fae73805376ea6bf88439783ecfba9aac88a43911b48c653437e784c4", size = 73303, upload-time = "2024-10-15T06:52:32.486Z" }, ] -[[package]] -name = "structlog" -version = "25.5.0" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } -dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ef/52/9ba0f43b686e7f3ddfeaa78ac3af750292662284b3661e91ad5494f21dbc/structlog-25.5.0.tar.gz", hash = "sha256:098522a3bebed9153d4570c6d0288abf80a031dfdb2048d59a49e9dc2190fc98", size = 1460830, upload-time = "2025-10-27T08:28:23.028Z" } -wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a8/45/a132b9074aa18e799b891b91ad72133c98d8042c70f6240e4c5f9dabee2f/structlog-25.5.0-py3-none-any.whl", hash = "sha256:a8453e9b9e636ec59bd9e79bbd4a72f025981b3ba0f5837aebf48f02f37a7f9f", size = 72510, upload-time = "2025-10-27T08:28:21.535Z" }, -] - -[[package]] -name = "taskgroup" -version = "0.2.2" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } -dependencies = [ - { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f0/8d/e218e0160cc1b692e6e0e5ba34e8865dbb171efeb5fc9a704544b3020605/taskgroup-0.2.2.tar.gz", hash = "sha256:078483ac3e78f2e3f973e2edbf6941374fbea81b9c5d0a96f51d297717f4752d", size = 11504, upload-time = "2025-01-03T09:24:13.761Z" } -wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d1/b1/74babcc824a57904e919f3af16d86c08b524c0691504baf038ef2d7f655c/taskgroup-0.2.2-py2.py3-none-any.whl", hash = "sha256:e2c53121609f4ae97303e9ea1524304b4de6faf9eb2c9280c7f87976479a52fb", size = 14237, upload-time = "2025-01-03T09:24:11.41Z" }, -] - -[[package]] -name = "text-unidecode" -version = "1.3" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ab/e2/e9a00f0ccb71718418230718b3d900e71a5d16e701a3dae079a21e9cd8f8/text-unidecode-1.3.tar.gz", hash = "sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93", size = 76885, upload-time = "2019-08-30T21:36:45.405Z" } -wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a6/a5/c0b6468d3824fe3fde30dbb5e1f687b291608f9473681bbf7dabbf5a87d7/text_unidecode-1.3-py2.py3-none-any.whl", hash = "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8", size = 78154, upload-time = "2019-08-30T21:37:03.543Z" }, -] - [[package]] name = "tinycss2" version = "1.5.1" @@ -3358,66 +1841,15 @@ wheels = [ ] [[package]] -name = "toml" -version = "0.10.2" +name = "tinyhtml5" +version = "2.1.0" source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/be/ba/1f744cdc819428fc6b5084ec34d9b30660f6f9daaf70eead706e3203ec3c/toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f", size = 22253, upload-time = "2020-11-01T01:40:22.204Z" } -wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/44/6f/7120676b6d73228c96e17f1f794d8ab046fc910d781c8d151120c3f1569e/toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", size = 16588, upload-time = "2020-11-01T01:40:20.672Z" }, +dependencies = [ + { name = "webencodings" }, ] - -[[package]] -name = "tomli" -version = "2.4.0" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/82/30/31573e9457673ab10aa432461bee537ce6cef177667deca369efb79df071/tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c", size = 17477, upload-time = "2026-01-11T11:22:38.165Z" } +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b1/1f/cfe2f6b30557c92b3f31d41707e09cef5c1efbd87392bc6c0430c46b0e4d/tinyhtml5-2.1.0.tar.gz", hash = "sha256:60a50ec3d938a37e491efa01af895853060943dcebb5627de5b10d188b338a67", size = 179242, upload-time = "2026-03-05T17:06:30.704Z" } wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/3c/d9/3dc2289e1f3b32eb19b9785b6a006b28ee99acb37d1d47f78d4c10e28bf8/tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867", size = 153663, upload-time = "2026-01-11T11:21:45.27Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/51/32/ef9f6845e6b9ca392cd3f64f9ec185cc6f09f0a2df3db08cbe8809d1d435/tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9", size = 148469, upload-time = "2026-01-11T11:21:46.873Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d6/c2/506e44cce89a8b1b1e047d64bd495c22c9f71f21e05f380f1a950dd9c217/tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95", size = 236039, upload-time = "2026-01-11T11:21:48.503Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b3/40/e1b65986dbc861b7e986e8ec394598187fa8aee85b1650b01dd925ca0be8/tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76", size = 243007, upload-time = "2026-01-11T11:21:49.456Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/9c/6f/6e39ce66b58a5b7ae572a0f4352ff40c71e8573633deda43f6a379d56b3e/tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d", size = 240875, upload-time = "2026-01-11T11:21:50.755Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/aa/ad/cb089cb190487caa80204d503c7fd0f4d443f90b95cf4ef5cf5aa0f439b0/tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576", size = 246271, upload-time = "2026-01-11T11:21:51.81Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/0b/63/69125220e47fd7a3a27fd0de0c6398c89432fec41bc739823bcc66506af6/tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a", size = 96770, upload-time = "2026-01-11T11:21:52.647Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/1e/0d/a22bb6c83f83386b0008425a6cd1fa1c14b5f3dd4bad05e98cf3dbbf4a64/tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa", size = 107626, upload-time = "2026-01-11T11:21:53.459Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/2f/6d/77be674a3485e75cacbf2ddba2b146911477bd887dda9d8c9dfb2f15e871/tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614", size = 94842, upload-time = "2026-01-11T11:21:54.831Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/3c/43/7389a1869f2f26dba52404e1ef13b4784b6b37dac93bac53457e3ff24ca3/tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1", size = 154894, upload-time = "2026-01-11T11:21:56.07Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e9/05/2f9bf110b5294132b2edf13fe6ca6ae456204f3d749f623307cbb7a946f2/tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8", size = 149053, upload-time = "2026-01-11T11:21:57.467Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e8/41/1eda3ca1abc6f6154a8db4d714a4d35c4ad90adc0bcf700657291593fbf3/tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a", size = 243481, upload-time = "2026-01-11T11:21:58.661Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d2/6d/02ff5ab6c8868b41e7d4b987ce2b5f6a51d3335a70aa144edd999e055a01/tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1", size = 251720, upload-time = "2026-01-11T11:22:00.178Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/7b/57/0405c59a909c45d5b6f146107c6d997825aa87568b042042f7a9c0afed34/tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b", size = 247014, upload-time = "2026-01-11T11:22:01.238Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/2c/0e/2e37568edd944b4165735687cbaf2fe3648129e440c26d02223672ee0630/tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51", size = 251820, upload-time = "2026-01-11T11:22:02.727Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/5a/1c/ee3b707fdac82aeeb92d1a113f803cf6d0f37bdca0849cb489553e1f417a/tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729", size = 97712, upload-time = "2026-01-11T11:22:03.777Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/69/13/c07a9177d0b3bab7913299b9278845fc6eaaca14a02667c6be0b0a2270c8/tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da", size = 108296, upload-time = "2026-01-11T11:22:04.86Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/18/27/e267a60bbeeee343bcc279bb9e8fbed0cbe224bc7b2a3dc2975f22809a09/tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3", size = 94553, upload-time = "2026-01-11T11:22:05.854Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/34/91/7f65f9809f2936e1f4ce6268ae1903074563603b2a2bd969ebbda802744f/tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0", size = 154915, upload-time = "2026-01-11T11:22:06.703Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/20/aa/64dd73a5a849c2e8f216b755599c511badde80e91e9bc2271baa7b2cdbb1/tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e", size = 149038, upload-time = "2026-01-11T11:22:07.56Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/9e/8a/6d38870bd3d52c8d1505ce054469a73f73a0fe62c0eaf5dddf61447e32fa/tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4", size = 242245, upload-time = "2026-01-11T11:22:08.344Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/59/bb/8002fadefb64ab2669e5b977df3f5e444febea60e717e755b38bb7c41029/tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e", size = 250335, upload-time = "2026-01-11T11:22:09.951Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a5/3d/4cdb6f791682b2ea916af2de96121b3cb1284d7c203d97d92d6003e91c8d/tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c", size = 245962, upload-time = "2026-01-11T11:22:11.27Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f2/4a/5f25789f9a460bd858ba9756ff52d0830d825b458e13f754952dd15fb7bb/tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f", size = 250396, upload-time = "2026-01-11T11:22:12.325Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/aa/2f/b73a36fea58dfa08e8b3a268750e6853a6aac2a349241a905ebd86f3047a/tomli-2.4.0-cp313-cp313-win32.whl", hash = "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86", size = 97530, upload-time = "2026-01-11T11:22:13.865Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/3b/af/ca18c134b5d75de7e8dc551c5234eaba2e8e951f6b30139599b53de9c187/tomli-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87", size = 108227, upload-time = "2026-01-11T11:22:15.224Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/22/c3/b386b832f209fee8073c8138ec50f27b4460db2fdae9ffe022df89a57f9b/tomli-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132", size = 94748, upload-time = "2026-01-11T11:22:16.009Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f3/c4/84047a97eb1004418bc10bdbcfebda209fca6338002eba2dc27cc6d13563/tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6", size = 154725, upload-time = "2026-01-11T11:22:17.269Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a8/5d/d39038e646060b9d76274078cddf146ced86dc2b9e8bbf737ad5983609a0/tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc", size = 148901, upload-time = "2026-01-11T11:22:18.287Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/73/e5/383be1724cb30f4ce44983d249645684a48c435e1cd4f8b5cded8a816d3c/tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66", size = 243375, upload-time = "2026-01-11T11:22:19.154Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/31/f0/bea80c17971c8d16d3cc109dc3585b0f2ce1036b5f4a8a183789023574f2/tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d", size = 250639, upload-time = "2026-01-11T11:22:20.168Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/2c/8f/2853c36abbb7608e3f945d8a74e32ed3a74ee3a1f468f1ffc7d1cb3abba6/tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702", size = 246897, upload-time = "2026-01-11T11:22:21.544Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/49/f0/6c05e3196ed5337b9fe7ea003e95fd3819a840b7a0f2bf5a408ef1dad8ed/tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8", size = 254697, upload-time = "2026-01-11T11:22:23.058Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f3/f5/2922ef29c9f2951883525def7429967fc4d8208494e5ab524234f06b688b/tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776", size = 98567, upload-time = "2026-01-11T11:22:24.033Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/7b/31/22b52e2e06dd2a5fdbc3ee73226d763b184ff21fc24e20316a44ccc4d96b/tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475", size = 108556, upload-time = "2026-01-11T11:22:25.378Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/48/3d/5058dff3255a3d01b705413f64f4306a141a8fd7a251e5a495e3f192a998/tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2", size = 96014, upload-time = "2026-01-11T11:22:26.138Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b8/4e/75dab8586e268424202d3a1997ef6014919c941b50642a1682df43204c22/tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9", size = 163339, upload-time = "2026-01-11T11:22:27.143Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/06/e3/b904d9ab1016829a776d97f163f183a48be6a4deb87304d1e0116a349519/tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0", size = 159490, upload-time = "2026-01-11T11:22:28.399Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e3/5a/fc3622c8b1ad823e8ea98a35e3c632ee316d48f66f80f9708ceb4f2a0322/tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df", size = 269398, upload-time = "2026-01-11T11:22:29.345Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/fd/33/62bd6152c8bdd4c305ad9faca48f51d3acb2df1f8791b1477d46ff86e7f8/tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d", size = 276515, upload-time = "2026-01-11T11:22:30.327Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/4b/ff/ae53619499f5235ee4211e62a8d7982ba9e439a0fb4f2f351a93d67c1dd2/tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f", size = 273806, upload-time = "2026-01-11T11:22:32.56Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/47/71/cbca7787fa68d4d0a9f7072821980b39fbb1b6faeb5f5cf02f4a5559fa28/tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b", size = 281340, upload-time = "2026-01-11T11:22:33.505Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f5/00/d595c120963ad42474cf6ee7771ad0d0e8a49d0f01e29576ee9195d9ecdf/tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087", size = 108106, upload-time = "2026-01-11T11:22:34.451Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/de/69/9aa0c6a505c2f80e519b43764f8b4ba93b5a0bbd2d9a9de6e2b24271b9a5/tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd", size = 120504, upload-time = "2026-01-11T11:22:35.764Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b3/9f/f1668c281c58cfae01482f7114a4b88d345e4c140386241a1a24dcc9e7bc/tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4", size = 99561, upload-time = "2026-01-11T11:22:36.624Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", size = 14477, upload-time = "2026-01-11T11:22:37.446Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/52/48/01695a036b695f83fea7aef6955d735db0f517b1c8e25ddb399ac0bdbcbf/tinyhtml5-2.1.0-py3-none-any.whl", hash = "sha256:6e11cfff38515834268daf89d5f85bbde0b6dd02e8d9e212d1385c2289b89f0a", size = 39686, upload-time = "2026-03-05T17:06:28.498Z" }, ] [[package]] @@ -3509,24 +1941,6 @@ wheels = [ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/fd/82/aab481e2fc6dee0a13ce35c750e97dbe3f270fb327089c99a8f5e6900e0c/ua_parser_builtins-202601-py3-none-any.whl", hash = "sha256:f5dc93b0f53724dcd5c3eb79edb0aea281cb304a2c02a9436cbeb8cfb8bc4ad1", size = 89228, upload-time = "2026-01-02T08:58:23.453Z" }, ] -[[package]] -name = "uncalled-for" -version = "0.2.0" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/02/7c/b5b7d8136f872e3f13b0584e576886de0489d7213a12de6bebf29ff6ebfc/uncalled_for-0.2.0.tar.gz", hash = "sha256:b4f8fdbcec328c5a113807d653e041c5094473dd4afa7c34599ace69ccb7e69f", size = 49488, upload-time = "2026-02-27T17:40:58.137Z" } -wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ff/7f/4320d9ce3be404e6310b915c3629fe27bf1e2f438a1a7a3cb0396e32e9a9/uncalled_for-0.2.0-py3-none-any.whl", hash = "sha256:2c0bd338faff5f930918f79e7eb9ff48290df2cb05fcc0b40a7f334e55d4d85f", size = 11351, upload-time = "2026-02-27T17:40:56.804Z" }, -] - -[[package]] -name = "urllib3" -version = "2.6.3" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } -wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, -] - [[package]] name = "user-agents" version = "2.2.0" @@ -3546,13 +1960,31 @@ source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } dependencies = [ { name = "click" }, { name = "h11" }, - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/5a/01/5e637e7aa9dd031be5376b9fb749ec20b86f5a5b6a49b87fabd374d5fa9f/uvicorn-0.30.6.tar.gz", hash = "sha256:4b15decdda1e72be08209e860a1e10e92439ad5b97cf44cc945fcbee66fc5788", size = 42825, upload-time = "2024-08-13T09:27:35.098Z" } wheels = [ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f5/8e/cdc7d6263db313030e4c257dd5ba3909ebc4e4fb53ad62d5f09b1a2f5458/uvicorn-0.30.6-py3-none-any.whl", hash = "sha256:65fd46fe3fda5bdc1b03b94eb634923ff18cd35b2f084813ea79d1f103f711b5", size = 62835, upload-time = "2024-08-13T09:27:33.536Z" }, ] +[[package]] +name = "weasyprint" +version = "63.1" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +dependencies = [ + { name = "cffi" }, + { name = "cssselect2" }, + { name = "fonttools", extra = ["woff"] }, + { name = "pillow" }, + { name = "pydyf" }, + { name = "pyphen" }, + { name = "tinycss2" }, + { name = "tinyhtml5" }, +] +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/2b/f0/1ac7d241b8cabaaf047278ef67b64869473a4e0a2218a1cbc0a6ffb0d8fd/weasyprint-63.1.tar.gz", hash = "sha256:cb424e63e8dd3f14195bfe5f203527646aa40a2f00ac819f9d39b8304cec0044", size = 491880, upload-time = "2024-12-10T15:51:29.034Z" } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d5/39/9d63960b4545138d6c2c695795d77856e35f30d6e4bdc385c848c816d349/weasyprint-63.1-py3-none-any.whl", hash = "sha256:9d0319fe3ba553c9a77dc43a2d35b64a70c2b8809ad55a139a214803fde62bce", size = 299994, upload-time = "2024-12-10T15:51:25.757Z" }, +] + [[package]] name = "webencodings" version = "0.5.1" @@ -3568,28 +2000,6 @@ version = "15.0.1" source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016, upload-time = "2025-03-05T20:03:41.606Z" } wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/1e/da/6462a9f510c0c49837bbc9345aca92d767a56c1fb2939e1579df1e1cdcf7/websockets-15.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d63efaa0cd96cf0c5fe4d581521d9fa87744540d4bc999ae6e08595a1014b45b", size = 175423, upload-time = "2025-03-05T20:01:35.363Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/1c/9f/9d11c1a4eb046a9e106483b9ff69bce7ac880443f00e5ce64261b47b07e7/websockets-15.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ac60e3b188ec7574cb761b08d50fcedf9d77f1530352db4eef1707fe9dee7205", size = 173080, upload-time = "2025-03-05T20:01:37.304Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d5/4f/b462242432d93ea45f297b6179c7333dd0402b855a912a04e7fc61c0d71f/websockets-15.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5756779642579d902eed757b21b0164cd6fe338506a8083eb58af5c372e39d9a", size = 173329, upload-time = "2025-03-05T20:01:39.668Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/6e/0c/6afa1f4644d7ed50284ac59cc70ef8abd44ccf7d45850d989ea7310538d0/websockets-15.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fdfe3e2a29e4db3659dbd5bbf04560cea53dd9610273917799f1cde46aa725e", size = 182312, upload-time = "2025-03-05T20:01:41.815Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/dd/d4/ffc8bd1350b229ca7a4db2a3e1c482cf87cea1baccd0ef3e72bc720caeec/websockets-15.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c2529b320eb9e35af0fa3016c187dffb84a3ecc572bcee7c3ce302bfeba52bf", size = 181319, upload-time = "2025-03-05T20:01:43.967Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/97/3a/5323a6bb94917af13bbb34009fac01e55c51dfde354f63692bf2533ffbc2/websockets-15.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac1e5c9054fe23226fb11e05a6e630837f074174c4c2f0fe442996112a6de4fb", size = 181631, upload-time = "2025-03-05T20:01:46.104Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a6/cc/1aeb0f7cee59ef065724041bb7ed667b6ab1eeffe5141696cccec2687b66/websockets-15.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5df592cd503496351d6dc14f7cdad49f268d8e618f80dce0cd5a36b93c3fc08d", size = 182016, upload-time = "2025-03-05T20:01:47.603Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/79/f9/c86f8f7af208e4161a7f7e02774e9d0a81c632ae76db2ff22549e1718a51/websockets-15.0.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0a34631031a8f05657e8e90903e656959234f3a04552259458aac0b0f9ae6fd9", size = 181426, upload-time = "2025-03-05T20:01:48.949Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c7/b9/828b0bc6753db905b91df6ae477c0b14a141090df64fb17f8a9d7e3516cf/websockets-15.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3d00075aa65772e7ce9e990cab3ff1de702aa09be3940d1dc88d5abf1ab8a09c", size = 181360, upload-time = "2025-03-05T20:01:50.938Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/89/fb/250f5533ec468ba6327055b7d98b9df056fb1ce623b8b6aaafb30b55d02e/websockets-15.0.1-cp310-cp310-win32.whl", hash = "sha256:1234d4ef35db82f5446dca8e35a7da7964d02c127b095e172e54397fb6a6c256", size = 176388, upload-time = "2025-03-05T20:01:52.213Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/1c/46/aca7082012768bb98e5608f01658ff3ac8437e563eca41cf068bd5849a5e/websockets-15.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:39c1fec2c11dc8d89bba6b2bf1556af381611a173ac2b511cf7231622058af41", size = 176830, upload-time = "2025-03-05T20:01:53.922Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/9f/32/18fcd5919c293a398db67443acd33fde142f283853076049824fc58e6f75/websockets-15.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431", size = 175423, upload-time = "2025-03-05T20:01:56.276Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/76/70/ba1ad96b07869275ef42e2ce21f07a5b0148936688c2baf7e4a1f60d5058/websockets-15.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57", size = 173082, upload-time = "2025-03-05T20:01:57.563Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/86/f2/10b55821dd40eb696ce4704a87d57774696f9451108cff0d2824c97e0f97/websockets-15.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905", size = 173330, upload-time = "2025-03-05T20:01:59.063Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a5/90/1c37ae8b8a113d3daf1065222b6af61cc44102da95388ac0018fcb7d93d9/websockets-15.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562", size = 182878, upload-time = "2025-03-05T20:02:00.305Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/8e/8d/96e8e288b2a41dffafb78e8904ea7367ee4f891dafc2ab8d87e2124cb3d3/websockets-15.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792", size = 181883, upload-time = "2025-03-05T20:02:03.148Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/93/1f/5d6dbf551766308f6f50f8baf8e9860be6182911e8106da7a7f73785f4c4/websockets-15.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413", size = 182252, upload-time = "2025-03-05T20:02:05.29Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d4/78/2d4fed9123e6620cbf1706c0de8a1632e1a28e7774d94346d7de1bba2ca3/websockets-15.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8", size = 182521, upload-time = "2025-03-05T20:02:07.458Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e7/3b/66d4c1b444dd1a9823c4a81f50231b921bab54eee2f69e70319b4e21f1ca/websockets-15.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3", size = 181958, upload-time = "2025-03-05T20:02:09.842Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/08/ff/e9eed2ee5fed6f76fdd6032ca5cd38c57ca9661430bb3d5fb2872dc8703c/websockets-15.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf", size = 181918, upload-time = "2025-03-05T20:02:11.968Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d8/75/994634a49b7e12532be6a42103597b71098fd25900f7437d6055ed39930a/websockets-15.0.1-cp311-cp311-win32.whl", hash = "sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85", size = 176388, upload-time = "2025-03-05T20:02:13.32Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/98/93/e36c73f78400a65f5e236cd376713c34182e6663f6889cd45a4a04d8f203/websockets-15.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065", size = 176828, upload-time = "2025-03-05T20:02:14.585Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/51/6b/4545a0d843594f5d0771e86463606a3988b5a09ca5123136f8a76580dd63/websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3", size = 175437, upload-time = "2025-03-05T20:02:16.706Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f4/71/809a0f5f6a06522af902e0f2ea2757f71ead94610010cf570ab5c98e99ed/websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665", size = 173096, upload-time = "2025-03-05T20:02:18.832Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/3d/69/1a681dd6f02180916f116894181eab8b2e25b31e484c5d0eae637ec01f7c/websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2", size = 173332, upload-time = "2025-03-05T20:02:20.187Z" }, @@ -3612,126 +2022,9 @@ wheels = [ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/33/2b/1f168cb6041853eef0362fb9554c3824367c5560cbdaad89ac40f8c2edfc/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4", size = 182195, upload-time = "2025-03-05T20:02:51.561Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa", size = 176393, upload-time = "2025-03-05T20:02:53.814Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837, upload-time = "2025-03-05T20:02:55.237Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/02/9e/d40f779fa16f74d3468357197af8d6ad07e7c5a27ea1ca74ceb38986f77a/websockets-15.0.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0c9e74d766f2818bb95f84c25be4dea09841ac0f734d1966f415e4edfc4ef1c3", size = 173109, upload-time = "2025-03-05T20:03:17.769Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/bc/cd/5b887b8585a593073fd92f7c23ecd3985cd2c3175025a91b0d69b0551372/websockets-15.0.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1009ee0c7739c08a0cd59de430d6de452a55e42d6b522de7aa15e6f67db0b8e1", size = 173343, upload-time = "2025-03-05T20:03:19.094Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/fe/ae/d34f7556890341e900a95acf4886833646306269f899d58ad62f588bf410/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76d1f20b1c7a2fa82367e04982e708723ba0e7b8d43aa643d3dcd404d74f1475", size = 174599, upload-time = "2025-03-05T20:03:21.1Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/71/e6/5fd43993a87db364ec60fc1d608273a1a465c0caba69176dd160e197ce42/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f29d80eb9a9263b8d109135351caf568cc3f80b9928bccde535c235de55c22d9", size = 174207, upload-time = "2025-03-05T20:03:23.221Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/2b/fb/c492d6daa5ec067c2988ac80c61359ace5c4c674c532985ac5a123436cec/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b359ed09954d7c18bbc1680f380c7301f92c60bf924171629c5db97febb12f04", size = 174155, upload-time = "2025-03-05T20:03:25.321Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/68/a1/dcb68430b1d00b698ae7a7e0194433bce4f07ded185f0ee5fb21e2a2e91e/websockets-15.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:cad21560da69f4ce7658ca2cb83138fb4cf695a2ba3e475e0559e05991aa8122", size = 176884, upload-time = "2025-03-05T20:03:27.934Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" }, ] -[[package]] -name = "whenever" -version = "0.9.5" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } -dependencies = [ - { name = "tzdata", marker = "python_full_version >= '3.13' and sys_platform == 'win32'" }, - { name = "tzlocal", marker = "python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux'" }, -] -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/cd/9a/6b12d5708a703010877bec0efc5172ae8a851516abc13cd4543ce4d02a20/whenever-0.9.5.tar.gz", hash = "sha256:9d8f2fbc70acdab98a99b81a2ac594ebd4cc68d5b3506b990729a5f0b04d0083", size = 259436, upload-time = "2026-01-11T19:47:51.608Z" } -wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f3/8d/e4a05aa4f44e9f01c887705915108098ec1088868c2ce2fb4cfa61ba9965/whenever-0.9.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:b110b89ed050b75ed6243c5e288a074b2853f85fb3af5d2a376f8792c6669ed8", size = 465829, upload-time = "2026-01-11T19:47:17.585Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a7/da/f3571e7bc9171553d8d490027fc245d1aad2dba6ad97c958c2916505267f/whenever-0.9.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:321bb20bd2ffae8ce44158326622b6b6961c5c96268b9720de3f43a0876e257f", size = 438160, upload-time = "2026-01-11T19:47:08.294Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/0b/54/122f82b9d299e141deaa7de7f1b399728ee14b115a0044c9d5fe9434bf26/whenever-0.9.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b20206f2c1f92aa6a2f1b5a1c1c6130fb85f01fe61b28718c4342b4dd4095308", size = 451932, upload-time = "2026-01-11T19:45:24.852Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/72/e8/a53247f4b03f712837ca56e5aa6cf0a2669001634862ee4238a61b048528/whenever-0.9.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d4b80a84f02d4e77fe7be45c7c2317c8a205bfd78568a0b6b516922086b14f44", size = 497340, upload-time = "2026-01-11T19:45:46.394Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/72/cd/1bdee24bf499d3f13c0b3e48e8233d6be17c29a2b1db4bfde1f4cc7ea48b/whenever-0.9.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3639728ee63d2ed6f007208f2212172f811afc46803a252aaf9f6d9f402fc85a", size = 485358, upload-time = "2026-01-11T19:46:06.113Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/83/a9/718015b5d7da6c1c65ec9a184f30365b15978b70648de1156dd6fab2fb74/whenever-0.9.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d2b77bb9e74c6790bec9397f31c9d40e79bd1029d3499d28185e44835b2d7b0", size = 520112, upload-time = "2026-01-11T19:46:16.469Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/7a/2f/9464b54835e8e47ef020fc2ec7af0edd951e7b0101e53fccffc63b6c5707/whenever-0.9.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:04a3a49d8439cfcbf37a95dabe7c6d91b425efccef87ea2f139d2d606136c247", size = 481063, upload-time = "2026-01-11T19:46:45.775Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/1c/fc/ebda1a35879e4c51f1b6fdf1eaddce533c6214803a8f1143d185959e615e/whenever-0.9.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:adb28b0fe1276cccfb9f4645f500191e731dad208351696e1a671f2e735a11e5", size = 519304, upload-time = "2026-01-11T19:46:26.463Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ea/92/351a3777593df001a556b7bae2bc1b7f2fbc75d91196ed3e15a7a205c525/whenever-0.9.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2cac529249c1c6f03a6fd4822afab1aae721ff21e43d98cac074df0c47123fdb", size = 636401, upload-time = "2026-01-11T19:45:35.99Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/1b/76/a89da281ae79843b61f23f9f984784fe8765a2d670a7f7f39c54b3e8e990/whenever-0.9.5-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:f1af8c973eb6d640b47ad45fdca25bfd4ca2b5257aa9e8be59baebd032a7b5a1", size = 768027, upload-time = "2026-01-11T19:45:55.397Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/91/ce/7c26b12900e81c2b1944773f1efe7615e83f89885f1d4a2d3b8a0e677e4c/whenever-0.9.5-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e4cc9b2178fdb2c350a264ead8f50d314e7c526e5fa063c8ccbc6da02fe7d62a", size = 730628, upload-time = "2026-01-11T19:46:35.891Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/cf/40/0e4b15eba6dd585615948e678566cea4654c4362c33f48c1c8de6a265c1c/whenever-0.9.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2af9c99d40dcdd76f92de749dad42e47017bc4057c26677a737631440d419369", size = 694047, upload-time = "2026-01-11T19:46:56.116Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/bb/f7/7876c7a42176c20b54a15786f670a449d104702c00e85912e86a0fb81b51/whenever-0.9.5-cp310-cp310-win32.whl", hash = "sha256:66d950fc79f904e4fad1eba838647896fcdf67b6cbdbc1f6d18c807600f922da", size = 413105, upload-time = "2026-01-11T19:47:28.285Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/07/2a/8a2628259aa5e93f1e6a0eb79376feb69912ee3a19d7604299ebbad05cd8/whenever-0.9.5-cp310-cp310-win_amd64.whl", hash = "sha256:f9b80fdd2f9d6e9c21fbbc908b76a7cbe234350b7652f46afba81481efc61cbd", size = 437870, upload-time = "2026-01-11T19:47:39.001Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/14/5f/6d450a9d65d2b6d057ee76d33755b16f90216c50fc30822a9ac7e54e48b8/whenever-0.9.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:7e0f576ef8bfe42a30ee58964aacbe808074af857db2c06e50fd52fbe823f781", size = 465828, upload-time = "2026-01-11T19:47:18.742Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a2/7f/68e7ae96d7496fa89b9a734aa7b39b67439a57fae2741d5852d8433086e8/whenever-0.9.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cc287533980f18f0979e43872e744486e9a021ba5712f04e957ca81b411d1d44", size = 438159, upload-time = "2026-01-11T19:47:09.377Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/15/ef/464eecf15a5b91154b3369e845b84b517190e201c7b2e941cdb072d438f6/whenever-0.9.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f39438186abfe8866f9e2aee02041cb1844d9d6a7e3b099d468ef00a73bdf1fb", size = 451933, upload-time = "2026-01-11T19:45:27.111Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/68/ca/7bcb3e87905b5d3cd05b3ca5a9641cb31498228d26ca2bc651d48b2c73fe/whenever-0.9.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:80c0f61c3c8489149dd4bb440d18ec36e17e4839faa4dc350a447f620842c43b", size = 497338, upload-time = "2026-01-11T19:45:47.769Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/93/68/cb439086d3666ea4a6ab01b449fea030657920d608f43d28d19d29e2ab05/whenever-0.9.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c639cd6d2e278cfbfd095b32158d7799be7479d4438b68ae46289f9c82781543", size = 485358, upload-time = "2026-01-11T19:46:07.159Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b9/6d/2bbb2bcb4e0669b77b92713a5b4a5e77257dbae5eaf4a3864c51a9ecfdbf/whenever-0.9.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4d039e03cbefd69a60886d14b5cb6ffba682bdce983d1eac9d1daa744072aa3e", size = 520113, upload-time = "2026-01-11T19:46:17.931Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c7/02/9b78a9d606029bcc38192cd2cc3ba01aedd94f80b0c468edda3108bf95b1/whenever-0.9.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d0da674f4f5a8a775c7abfec4201c64741a1b932a281b9f3f32e378f5e6d7894", size = 481062, upload-time = "2026-01-11T19:46:46.809Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/dd/c1/2cf6e1855ca670f9b3b67ac3dd9c8bc64c0800a7a7a1f4d88f480d2b5fd9/whenever-0.9.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:856223ea8b315bfd19252e72aadc9d0ff3d8d2e40e9824a37ed577193ea7e76b", size = 519304, upload-time = "2026-01-11T19:46:27.734Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/0d/49/4028de789a3b88e8f143190dc5bc18fbeb82fdbf0e95a245fb0e9dc13ef7/whenever-0.9.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:587d044067b952277440510526319e9b67219112813b03229b7d2930a616f027", size = 636403, upload-time = "2026-01-11T19:45:37.276Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/5a/a3/41b224c6b9eaf1b999557f10f6ba9844d6c1c73b4a5dbb8fd9994c8895c9/whenever-0.9.5-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:3eaa851b6a5cbb56deaf8fe192cf4398f846497bbefb425c19870bd0d62af0aa", size = 768029, upload-time = "2026-01-11T19:45:56.367Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/14/4a/ad6f092c605f2a202170a4ec86003444e8549d2d9ebad4e38e2f0893bb57/whenever-0.9.5-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:64479b06ab2cbc3a02477857ad2fb8943a4ff8f1480aaa13125080707932f915", size = 730631, upload-time = "2026-01-11T19:46:37.096Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/6c/fb/872390afc9d7f7d698b0c52e3f0b4f4a5ba14b29d2f32027eef22d63d555/whenever-0.9.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:926acd3e6a8d37b246df7486e3ab9562c413ed90a814cf137e9eca11c6ec6b13", size = 694050, upload-time = "2026-01-11T19:46:57.695Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f5/dd/2000edaba7e1874b4c340a4f7b3022c55f0541f99758e3ed92df3719f679/whenever-0.9.5-cp311-cp311-win32.whl", hash = "sha256:1c3ee48230266bc27193586fe77cb2395079de7b3bc0cbff00d87097fc1ec235", size = 413105, upload-time = "2026-01-11T19:47:29.766Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/0e/34/35ba090f384d42e7b203e7c5d467e6a1944c151bb0a040948f91d36d6777/whenever-0.9.5-cp311-cp311-win_amd64.whl", hash = "sha256:283e73a8565c827ff9576926765d3e4f3d41d7dec38e1b63e7faadb5b233ddec", size = 437867, upload-time = "2026-01-11T19:47:40.496Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c6/7d/f05ea580a1c99575c82a1b15d6c64826cb0e53101ad06020df109a51aa29/whenever-0.9.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:59c65af262738a9f649e008d51bbba506223ca7a7f2a5c935b9b0975f58227ed", size = 467312, upload-time = "2026-01-11T19:47:20.275Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/dc/6d/15102f4703ce111b1f1066afb2d62c40575ad2be91c7896442a77bf4d937/whenever-0.9.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a957d5e2bb652cb802eee2e1683c62023a9ba696e82483c4629b829f6c16fb5d", size = 438830, upload-time = "2026-01-11T19:47:10.453Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/1c/cb/a603e92c85b3d70b64380e737d080329fd731d3ab2ef30475d099bb1f85a/whenever-0.9.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f5e6da8f343a7522f1b2830b0e365e5417086da7a67d3086fbfc83d332b55c56", size = 453060, upload-time = "2026-01-11T19:45:28.885Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b0/36/a9f07ec85ddc415f3500d00acdfeb5b63a6dcaba91f2802e1061f4a32240/whenever-0.9.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:767f34bd6dd209afaebebb481de3af03d9a61271f5455c7e4dffedfac63ed6e9", size = 498406, upload-time = "2026-01-11T19:45:48.771Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/7a/f8/bb0891fce7605b44d0dc4d0a09c27dd8408e75e90c1f1adff1a3ef6ab0c0/whenever-0.9.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:42d707e3f2c07a0c688c27c16f3e9bbe56551b72ec1fb9bc8df32b8d45e54520", size = 487042, upload-time = "2026-01-11T19:46:08.306Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/66/35/3ea67d9e4fbebb619dc7801d678b3b40408586619a8bf0a18c7d20e9dc91/whenever-0.9.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6e0a004b2b143146dbfafcf023b7bf007333e73c2d0955f44762a3131cbfcb47", size = 521880, upload-time = "2026-01-11T19:46:18.987Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/62/5f/24f6c09a4f1e66b9257b88711164cad6262dc8c8d51d1a758d144e26f249/whenever-0.9.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d9b760e8968f1d2681687dfd0ac06d7cb310603710faad0b85d3d9b86ac792f3", size = 483374, upload-time = "2026-01-11T19:46:48.425Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/93/af/335af2917c6fdb58fd01dcf4922dd5d251ce70387d9bea60e723d45a01ae/whenever-0.9.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e7fb3da8014c3170b53a629b278105bcb3679c0835db7d88c27798058686181a", size = 521134, upload-time = "2026-01-11T19:46:28.778Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/dc/05/fa3c60413befbd2c5c52fa9c65b840bcb4d73e8ed32e2df69696640481cc/whenever-0.9.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ce9a6d8d05db2aefb13b0f49d4a581b13035f966c967cd3e7f98a2bb6016ab66", size = 636617, upload-time = "2026-01-11T19:45:38.49Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/3c/56/63668cd8ae23c0219e75b254ba926425d44ddb3482f6c95aac30b00cee2f/whenever-0.9.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b9c52bd3ff2246e9c03d8e72c5d28044f35070b7c23d44b93592cd5b61c45f36", size = 769639, upload-time = "2026-01-11T19:45:57.548Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/25/ff/3781f3867897974b39eea74a83f337479d98c76cf74608b8b6fa9c7b59a7/whenever-0.9.5-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:77086386feba0bee7bc030b86e118a521c18b3741ad3c69e0c8e7b601a763a2a", size = 733169, upload-time = "2026-01-11T19:46:38.25Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e6/85/09d9e4e17ef78b380e57c6b89274fa37c8b250321d5e09f9efa8a58c941e/whenever-0.9.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:45ae1ce8bbb3668f6281251a0d22ac81bdf563909306b0d69eaa630ceb1051b9", size = 696787, upload-time = "2026-01-11T19:46:58.887Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/0b/96/78408214f359016c0153f103decc6e3566426f809ea0c250d638b4315de6/whenever-0.9.5-cp312-cp312-win32.whl", hash = "sha256:6f2d87720d45a891f606420fcc073796740266f1bca718607569d44b7e0e6639", size = 416143, upload-time = "2026-01-11T19:47:30.894Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/58/4d/ef3898377abf417a9e30692c1299cb0623f1f42ec6f8d8ba07809ca58eab/whenever-0.9.5-cp312-cp312-win_amd64.whl", hash = "sha256:a5bfb110948bc668728dbb87f8985e6004dedf37ed910a946ddbe31985ada006", size = 440123, upload-time = "2026-01-11T19:47:41.822Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/48/c7/f5626d4cc477116e6ec3191515981a60cdd021adc97046cb422f6bed327c/whenever-0.9.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:d7de2cac536fe28f012928b74e0a3ed15dd8f4ac9940cfce35104cca345937f0", size = 467317, upload-time = "2026-01-11T19:47:21.582Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ea/81/d59f0e226ef542fc4bc86567d7b9e2bf9016c353b1f83661ee3913a140a7/whenever-0.9.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e00bc8f93fa469c630aad9dfdc538587c28891d6a4dce2f0b08628d5a108a219", size = 438838, upload-time = "2026-01-11T19:47:11.553Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b5/dc/090732e6e75f15a6084700d3247db6aa1f885971b637531529c62c4ba1c6/whenever-0.9.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9ac83555db44e1fcfc032114f45c09af0ed9d641380672c8deb7f1131a0fd783", size = 453069, upload-time = "2026-01-11T19:45:30.238Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ab/f9/b4744370a9491689fc46327a17e3246a9e107b19efa0e24f51d9dcbe6aeb/whenever-0.9.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1c486b1db0b833a007e2b8264f265079e57593925131b83ab69514fb9563b8d9", size = 498413, upload-time = "2026-01-11T19:45:49.811Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/62/78/45ef94ef51cb97c514650c2af33cdfc3de3cf967fcf12955c0e3a430a6cf/whenever-0.9.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8a57e8f207b5ad9b0c6bcd3efee38d65a70434c1bdcb95cc5d2e2b7a5237c208", size = 487043, upload-time = "2026-01-11T19:46:09.528Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/6b/23/f45082a60471ee79edc5bf5c09c1d4f55324a2c740fb138a3a691f19d2b6/whenever-0.9.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7e1d66a0ece1dabd6952eb7b03a555843bada42bed4683ea71b113f194714144", size = 521879, upload-time = "2026-01-11T19:46:20.093Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/56/5c/8d6dc529595b5387f5727cd6c2c5b8615851d95fec5c599a61ef239cc1b3/whenever-0.9.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b4056aaff273a579f0294e5397a0d198f52906bbaf7171da0a12ecd8cdf5026c", size = 483388, upload-time = "2026-01-11T19:46:49.746Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/29/52/366c4658286a5986acb76473e9e21457af2bd1b53dc050cee00160106eaa/whenever-0.9.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9b0e7c669be2897108895f3283e23fe761782c9c1abb0ce8834e57e09ce5cdfd", size = 521135, upload-time = "2026-01-11T19:46:30.122Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/9a/cb/82bbe5ce0879dd38d201214323afad418f1d721111311807c75aaf2e2d16/whenever-0.9.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9e0c7a146da12c37d68387c639f957e3624cf7a55a1d534788d5aea748a261e7", size = 636617, upload-time = "2026-01-11T19:45:39.656Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ec/32/8dcb6facf229b8e2b5c73d0c50adf800593769e34ab5abcd77828451c4ed/whenever-0.9.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:70c5f3013d6a1b7c40adbc05bc886e017f2b981739aaceafce47069da5b7e617", size = 769643, upload-time = "2026-01-11T19:45:59.047Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/eb/40/6ab4308550851209982b78692d14a9729bf20b6e814ddedf2658e46a6cd8/whenever-0.9.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:a8ce45c7271b85292e9d25e802543942b0249e73976c8614d676f0b561b25420", size = 733170, upload-time = "2026-01-11T19:46:39.457Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/47/38/7ad290229e8df99107d04e21ecef9d09f215428ab3aa06c0e46d51cba860/whenever-0.9.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f1db334becda2d7895bc31373b1e53ff73e3a0ea208e2845b4f946ad9a5e667", size = 696797, upload-time = "2026-01-11T19:47:00.001Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ff/8e/94c4fba5ef73bb817c557e19bb75665cb5761e4badfb60950bd4d3cc5a01/whenever-0.9.5-cp313-cp313-win32.whl", hash = "sha256:4079eabfa21d1418bcc2c3d605e780cfec0ad8161317e12fa0f1ef87cb08fe7d", size = 416152, upload-time = "2026-01-11T19:47:32.083Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/81/b4/17d4bc76ca73c21eb5b7883d10d8bacb7ce7a30a8f36501db2373c63ffb3/whenever-0.9.5-cp313-cp313-win_amd64.whl", hash = "sha256:16497a2b889aeeb0ee80a0d3b9ce14cdb63d7eb7d904e003aae3cd4ac67da1e8", size = 440135, upload-time = "2026-01-11T19:47:42.963Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/30/d3/13db725dfef188bec32e51128e888a869ee97d26d965cfe39edf1321ca8f/whenever-0.9.5-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:f222b1d59cdeadc1669657198e572727db8640e0b534e4e1ad0491417ec98379", size = 466415, upload-time = "2026-01-11T19:47:22.7Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a6/e9/4a9deeb616fbdf8e0c75fc78f13c37270d1d7e7ec776ffdf306262b8c593/whenever-0.9.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:6f4ae436a4db888cd5b48a88fb83ae3f70c468a7d0018cc9572e26e99b9f591f", size = 437247, upload-time = "2026-01-11T19:47:13.1Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/15/02/21866bddf5f819f1a3a59463f4637755590cb4045d25a50fb6ab60db97a6/whenever-0.9.5-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68b7c201fc49123159b1518147e48791e95a19cba0b3ebb646a446eb7e95148f", size = 451556, upload-time = "2026-01-11T19:45:31.645Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/51/14/2c84db8f23577ddfd6ecd90c8d192d21dfa75aea57cda44eb53048b27e69/whenever-0.9.5-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5d2a8dd9c565eacb199ff704d96fc95e74f6c324639d970cac3b4d80eefbaebb", size = 498034, upload-time = "2026-01-11T19:45:50.926Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/cc/37/cb69a10573382bbfd14954b5386bf5c47290551246d497a9aa733a5b5a8a/whenever-0.9.5-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e0d965ee9dafe1eb37ec273f7520e26b70d8b1bb075c06b0bb9e434363ca66fd", size = 487381, upload-time = "2026-01-11T19:46:10.752Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/47/c4/fcc0f886322f72c5d83ee669ed1aa253c4e58625e5bf64fede03a69aa7a0/whenever-0.9.5-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:035594337920cc9b9eb4531a97037971c109cc60e9a7a53e49e65722a1db7535", size = 520975, upload-time = "2026-01-11T19:46:21.789Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/29/cc/ad7e9c225a97be16b97a20c46802991908a12f9920d83071d669ed7ed79c/whenever-0.9.5-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26c5b04d6fcb0e4ee9f6e62e4400fd99c5f188449314f5863a865be33a653984", size = 481911, upload-time = "2026-01-11T19:46:50.87Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ea/12/4fa12c102d6aa3190ce1f34dc9f709776e9c3b1537328c46b6444b9a1638/whenever-0.9.5-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04755e5a2b802451332d2a18bf1c45d74f99cd16e61a0724fe763907f32cf7f8", size = 520068, upload-time = "2026-01-11T19:46:31.175Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a7/e7/08d7c7e59130e8379240fda96cd68ae60435f6d6b6e356a5900c6856e48f/whenever-0.9.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:0f7a37894b9cfc1e68b23f25d19ac38da68454f6bf782eab4cfac58578092daa", size = 635235, upload-time = "2026-01-11T19:45:41.389Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c1/6d/49c5566c5560606659dfed8dd49cadfa4d6a14b3f0d7a3d5a46a0c4e5624/whenever-0.9.5-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:55f10416bed357e0d1d1e4bf5e625974fe0a01348117161b4a0d2a69361c0efc", size = 769640, upload-time = "2026-01-11T19:46:00.112Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b8/09/e05cdc29897b97f3e3c0ffd485bf165f779c6d89bc4c0c0a76592e7c2e8c/whenever-0.9.5-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:75bfb8c778db727230fa5e7e2e256615be287d00cb1132b93789525e4d1c5dc6", size = 731095, upload-time = "2026-01-11T19:46:40.693Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/1c/d1/dcb635c2951ed5a112d340f73bf3cc81d69d1af7c0b1a33d0ca9d3299a26/whenever-0.9.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5bdc5ca9cddc9264d587469d994e046ae9c55cfdb6b9048927fe2922ae8763a1", size = 694782, upload-time = "2026-01-11T19:47:01.532Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/56/06/1544751be5e96aeefb59b5fedc1f8f4b0f238197ce21496d5960da20f18c/whenever-0.9.5-cp313-cp313t-win32.whl", hash = "sha256:274b7acfdc1fdfb29ab55299ce0a90cabedd079ff9940e24d05e3de4b04e1638", size = 415984, upload-time = "2026-01-11T19:47:33.34Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ab/0e/c87cf223816731718e5fef792b1f8c6d124c9fcbec677d4b7c9fcfe8f44f/whenever-0.9.5-cp313-cp313t-win_amd64.whl", hash = "sha256:7c9429fa1249aa39385716644453d9a4de0974b537974988b175d3ba7175a43f", size = 438641, upload-time = "2026-01-11T19:47:44.295Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d7/e4/5e78fc3b8a2bc84e0f055a89a9f4eabc06bc435463ea112265e1e73592f5/whenever-0.9.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:e381120682f6675cccbcba13b4c88b033f31167f820c161936fa99556e455f03", size = 468861, upload-time = "2026-01-11T19:47:24.176Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e8/a3/bd597dff8c82b2a1568813015ff25f3df2b6817872c8061176f04e3aad0f/whenever-0.9.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:caa7be36a609d765b9ea1c49159c446521b6ac33ff60b67214aa019713e05dda", size = 441322, upload-time = "2026-01-11T19:47:14.184Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/82/3c/fcb3a85cfdc12783c22d44640be538f78bcc3946cc3fb454139cdaebd119/whenever-0.9.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:037fd64e27df6116c3ffe8e96cdb6562051b0eda3f0bc7c35068fe642a913da9", size = 455230, upload-time = "2026-01-11T19:45:32.978Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e9/17/83c83dddb55f7576f3748b077774d2bd8521bc2220bd2fa6f711ad8f046e/whenever-0.9.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5c864363a2ff97c0b661915112140b9229132586d035ee4989cbe914aa3e3df4", size = 499473, upload-time = "2026-01-11T19:45:52.257Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/07/11/406d26cf641ef9e3cdd0f11237a2a49b06f225c5f57257850a2e101c8760/whenever-0.9.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e3d2c3f7bc9ceaef7d58fb40f0a4602a1947691eac1bff2623d2f923d4374543", size = 488680, upload-time = "2026-01-11T19:46:12.382Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b5/15/7d85e7f5710fc4dfaec830da0b069791ca6b7108939ab06a0440cfd11f7a/whenever-0.9.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ce45c364734e63a459b3b816f1ce159bb4d4f570faa2b0fa504d6d329dd654a3", size = 522715, upload-time = "2026-01-11T19:46:22.835Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/3f/53/ec6eb7d71624437571ed22c9fbfa7c8532b6868c94138e0775f61c69101c/whenever-0.9.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8fc8e6e500f60299be739e19ffb830b2d9f50da017e14cd832b31f31ee93166", size = 484518, upload-time = "2026-01-11T19:46:51.971Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/17/48/220d4a7335d91face9e5c16ec5228ad454ff58cfb3713c291c90d54b3e84/whenever-0.9.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b422bb8651f094f4420749f4c0d72287121480a495c6d247035bd52a72c4586c", size = 522611, upload-time = "2026-01-11T19:46:32.224Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/be/fd/d1b270585f459f469af7c08e03ba2bcfe058449162354bf809e671ccfb1e/whenever-0.9.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:87635742acdd0adbbed62c6ed2ec4a49e25b1e3c7374f13f39d2c5f9229ae24f", size = 638421, upload-time = "2026-01-11T19:45:42.843Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/2f/0c/7fc33cdb8da7d2b14cd7129f37b48c1aa91644eabc07780333979bdf15f6/whenever-0.9.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:1ae39fff83ad73ec563db65707e5bde1e74b350bf4309cfb265d527a6cd222c6", size = 770967, upload-time = "2026-01-11T19:46:01.614Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/fc/39/a379dcf478136619050cee1ed789a9bdb0a6537fe8c43b23ea832e38c8ce/whenever-0.9.5-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ae5b01eb93c3ad703d7870d9e6566c389b889e97d7efec30c73abf64aa95b47f", size = 735292, upload-time = "2026-01-11T19:46:41.75Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/09/a7/139ac3b81239f1d6275e8eb792c52a48adc6ba303b7e0c9d8ca405a86c76/whenever-0.9.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5e278daa1e650958f9ba7084b1c3c245444acf3e30db9cca5141658347a7c67d", size = 697998, upload-time = "2026-01-11T19:47:02.681Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/2e/37/141041e40f76dd530183c715faae1d8b21e47867fe159b8b88c4960eae2d/whenever-0.9.5-cp314-cp314-win32.whl", hash = "sha256:1b62c62a00bd93e51f71d231aef3178f1c22566b2818c190e755c96fd939b91a", size = 418248, upload-time = "2026-01-11T19:47:34.855Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/4b/7b/c86657bec1a679042c239a247bc3e6a92f3acc53ec858d13c1e770777f41/whenever-0.9.5-cp314-cp314-win_amd64.whl", hash = "sha256:483c2736ce367dbda01133700b064dfa8b6ed24833fc984e1d767e81aa02a189", size = 442418, upload-time = "2026-01-11T19:47:45.727Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/55/3b/2d37c3438214a1bf6caddddf825d5ff86e6cfcdf91a41fa1175334a2bab0/whenever-0.9.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7644b6c35f9c1fa7b7f6cb03bf5764207374bf3e2049cb49e7578648b0d27dd9", size = 468132, upload-time = "2026-01-11T19:47:25.341Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ba/0d/284336cf592107ee65c61030f1ca9717214365661d642aadf9fbce38bf8d/whenever-0.9.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:be31d5799b49e593da413cddc73728a51d0e80461262c7a4ed66b95819a69016", size = 438449, upload-time = "2026-01-11T19:47:15.283Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/5a/50/a13b1869b06a8838fab79e31707bf882f836764dcf1260ce170c1084011b/whenever-0.9.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6691e1157026773354c77771cd72db727bbd0ee36121cd139cf16f03cddc1699", size = 452750, upload-time = "2026-01-11T19:45:34.047Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/79/9d/3b9006df1289c78a2e3b8bdd6498430ffee57cb851c938a9da9a7f2ca11d/whenever-0.9.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7e31f4bb592cdd433dde40acef83f968b89178b42169724e0b200a318536c4f", size = 497651, upload-time = "2026-01-11T19:45:53.477Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c3/94/6e66e55649ed8c2e742ca3284e3dd158b0cb3d1272fc7bacc5b0938eba12/whenever-0.9.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9f8100609213040dda06e1d078bc43768d5d16dbaabfa75b13c5adb5189056be", size = 488006, upload-time = "2026-01-11T19:46:13.624Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c2/62/5c0bea83a518f3a76377400bdc29215d2f964be4d1bc8d3886f91824a4cc/whenever-0.9.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:38c415a27661f830320c96f3cf6af99ea96b712c9c3a14975ff8c34263d125c6", size = 521517, upload-time = "2026-01-11T19:46:23.874Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/80/0a/e93c2ccfb993d5ac1434913d9a2cea2e79d93affbbec06942c748d04681f/whenever-0.9.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cee6fce59f948dd30c01107d55d40d2c035c6dfd3005c49d535e10175832601c", size = 483401, upload-time = "2026-01-11T19:46:53.531Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/4d/05/d142ca3b56a1012082d0d2b71f9e9e1b9b42ba266d92294f841cb7acc07e/whenever-0.9.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ef181122ffcef41551d5eef483bf12ea48d1115554911a674e404e640ef0fd26", size = 520096, upload-time = "2026-01-11T19:46:33.746Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ca/b9/53a3ebb2c44e7215fa8c6720361a5c78891538f9c934935ffbf508801aae/whenever-0.9.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e4970bc5c37d4fff22e299314bebebb2d1a88133a67a51f87013dad0ac6535fb", size = 637251, upload-time = "2026-01-11T19:45:43.849Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ea/17/4f15ae9977972a7b8cd2819c4f03b63884416e4064d6d7366164cd6093f4/whenever-0.9.5-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:db8043800bd6eba7e681a316cba3734c8d10dad044b4a0dcf5fb0129665902ce", size = 769681, upload-time = "2026-01-11T19:46:03.041Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a0/a5/280dd222b31f478cef551f0d6554ab224e436b7b4ba1456d3cad864de756/whenever-0.9.5-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:067235cd1be14f709219eae4e6ae639ec19418e7860d1be1e59c5e4bd9cb3542", size = 732487, upload-time = "2026-01-11T19:46:43.395Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/8d/33/d2bed59cfe7cf358e78bfa26bad4414a0a3d99c6aa5e5b76aea448661bae/whenever-0.9.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:81556be508452f5fe7d7e7837718ff91ddb16089a705febe178fa8baabc99236", size = 696906, upload-time = "2026-01-11T19:47:03.814Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/36/4a/b27b182fdf24c9760a91481c6b97d9f536a15f6bc7c33d750eccaa245670/whenever-0.9.5-cp314-cp314t-win32.whl", hash = "sha256:681c9f4d322182df28037a33f0d4eadf7577fcd590130f7a5d23763a86ab5687", size = 417599, upload-time = "2026-01-11T19:47:36.201Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/6d/93/712f65c4fc3c188b0c163cd650cb04a46f993a22d1ad3f66a3e4a42a39f8/whenever-0.9.5-cp314-cp314t-win_amd64.whl", hash = "sha256:37242c895078bb1bfc26bea460f6d92dc5bdfe4a59e95a4f9e01f99b90f0157e", size = 440654, upload-time = "2026-01-11T19:47:46.924Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/91/fb/1b112af72e5a2b5ac66b5a6a478cc220117224c1481a2b1a35e87222a791/whenever-0.9.5-py3-none-any.whl", hash = "sha256:cc600303e703d57cc42c5db1c81bbc3becb8ef6fde5c46aee5d68c292239fc4b", size = 64873, upload-time = "2026-01-11T19:47:49.585Z" }, -] - [[package]] name = "win32-setctime" version = "1.2.0" @@ -3742,10 +2035,17 @@ wheels = [ ] [[package]] -name = "zipp" -version = "3.23.0" +name = "zopfli" +version = "0.4.3" source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" } +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/74/21/3b6af43a663b22b00e738bb0642931a2579e15da6852613d56c6aa535d28/zopfli-0.4.3.tar.gz", hash = "sha256:d3a50f91a13cea9bafe025de8fd87a005eb26de02a4f0c193127ddbf23ac8ebe", size = 179156, upload-time = "2026-06-10T09:10:19.96Z" } wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a5/5f/b7d81b670daf990e15a0f7551da96c3c0700f69ae6d96b0245d6a19f51f3/zopfli-0.4.3-cp310-abi3-macosx_10_9_universal2.whl", hash = "sha256:88f4fbe429aad72bc206275d81fab11a097e0f951a5848d1f51083c37ea73073", size = 291492, upload-time = "2026-06-10T09:10:06.621Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/55/c8/d8d8d731e0b192024567b7198fb77b748821d355f3c8bf0109de27191f43/zopfli-0.4.3-cp310-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:769875152d0625c46707bcca57d4b2233fe653482067acd55fbf6ec525cb9bdc", size = 829354, upload-time = "2026-06-10T09:10:07.909Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/0e/2b/fbe8ba2ec40f5986b8983a4752f7a32672a80a10ea6e68213324a7055469/zopfli-0.4.3-cp310-abi3-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb0c9c1d40a8cb1d58762d7e57290ccb753e0828c4d01be8acb59aae5d0ca206", size = 818436, upload-time = "2026-06-10T09:10:09.063Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/de/d9/63568c54c8b68b9135f3456c5add83797a5528d596657f0e4f4910173b08/zopfli-0.4.3-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7fa3c35193475290e3f007bbcdebdbae64ba2f012d75c632da0d727e1da50d5e", size = 1778931, upload-time = "2026-06-10T09:10:10.282Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/7a/05/8f3aac10a858e89c2146d3a1f6ce33634c3db757365b4148fef1b85784d2/zopfli-0.4.3-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:47604eee5c6704bdf0e94d8391fe3b74ddb2abd84128fbcfdc3ee0fc265feaef", size = 1864132, upload-time = "2026-06-10T09:10:11.595Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/8d/20/9ca59d14b91f9fbc631793b4b085b309777edadaca496aa518a180817827/zopfli-0.4.3-cp310-abi3-win32.whl", hash = "sha256:628c3e941752880b3491db8d44163d0aedb221944e22a17187ff7fc549b050f6", size = 271715, upload-time = "2026-06-10T09:10:12.7Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/9d/3a/4ff4fdead77ef30f5832b38a47eb7a1283e98b3c678576b83f8fdfff53eb/zopfli-0.4.3-cp310-abi3-win_amd64.whl", hash = "sha256:921c2c9907f4364963848da5ad194b46d68865e07fdb975d04fd09bc42d47357", size = 288550, upload-time = "2026-06-10T09:10:13.639Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e6/44/6264f929057236fde72dd6d271f54612b4811ce37288e002f5d5339d696a/zopfli-0.4.3-cp310-abi3-win_arm64.whl", hash = "sha256:7e9703ca6e7ef66c8d05e0826b6f558b680c9db8206f84f05a3ee93430a12e42", size = 451343, upload-time = "2026-06-10T09:10:14.72Z" }, ] diff --git a/frontend/web/src/api/module_monitor/online.ts b/frontend/web/src/api/module_monitor/online.ts index 59c7f3d9..68feb5db 100644 --- a/frontend/web/src/api/module_monitor/online.ts +++ b/frontend/web/src/api/module_monitor/online.ts @@ -42,7 +42,7 @@ export interface OnlineUserTable { session_id: string; user_id: number; tenant_id?: number; - is_super_admin?: boolean; + is_superuser?: boolean; name: string; user_name: string; ipaddr?: string;