diff --git a/backend/app/admin/service/dept_service.py b/backend/app/admin/service/dept_service.py index fd572dc1..aca0673e 100644 --- a/backend/app/admin/service/dept_service.py +++ b/backend/app/admin/service/dept_service.py @@ -7,8 +7,6 @@ from backend.app.admin.crud.crud_dept import dept_dao from backend.app.admin.model import Dept from backend.app.admin.schema.dept import CreateDeptParam, UpdateDeptParam from backend.common.exception import errors -from backend.core.conf import settings -from backend.database.redis import redis_client from backend.utils.build_tree import get_tree_data @@ -115,8 +113,6 @@ class DeptService: if children: raise errors.ConflictError(msg='部门下存在子部门,无法删除') count = await dept_dao.delete(db, pk) - for user in dept.users: - await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}') return count diff --git a/backend/app/admin/service/plugin_service.py b/backend/app/admin/service/plugin_service.py index 1ebd7728..339bb2b4 100644 --- a/backend/app/admin/service/plugin_service.py +++ b/backend/app/admin/service/plugin_service.py @@ -75,7 +75,7 @@ class PluginService: bacup_dir = PLUGIN_DIR / f'{plugin}.{timezone.now().strftime("%Y%m%d%H%M%S")}.backup' shutil.move(plugin_dir, bacup_dir) await redis_client.delete(f'{settings.PLUGIN_REDIS_PREFIX}:{plugin}') - await redis_client.set(f'{settings.PLUGIN_REDIS_PREFIX}:changed', 'ture') + await redis_client.set(f'{settings.PLUGIN_REDIS_PREFIX}:changed', 'true') @staticmethod async def update_status(*, plugin: str) -> None: diff --git a/backend/app/admin/service/user_password_history_service.py b/backend/app/admin/service/user_password_history_service.py index 4a1655df..36314bef 100644 --- a/backend/app/admin/service/user_password_history_service.py +++ b/backend/app/admin/service/user_password_history_service.py @@ -57,11 +57,19 @@ class UserPasswordHistoryService: failure_count = await redis_client.get(f'{settings.LOGIN_FAILURE_PREFIX}:{user_id}') failure_count = int(failure_count) if failure_count else 0 failure_count += 1 - await redis_client.set(f'{settings.LOGIN_FAILURE_PREFIX}:{user_id}', str(failure_count)) + await redis_client.setex( + f'{settings.LOGIN_FAILURE_PREFIX}:{user_id}', + settings.USER_LOCK_SECONDS, + str(failure_count), + ) if failure_count >= settings.USER_LOCK_THRESHOLD: locked_until = timezone.now() + timedelta(seconds=settings.USER_LOCK_SECONDS) - await redis_client.set(f'{settings.USER_LOCK_REDIS_PREFIX}:{user_id}', timezone.to_str(locked_until)) + await redis_client.setex( + f'{settings.USER_LOCK_REDIS_PREFIX}:{user_id}', + settings.USER_LOCK_SECONDS, + timezone.to_str(locked_until), + ) raise errors.AuthorizationError(msg='登录失败次数过多,账号已被锁定') @staticmethod diff --git a/backend/app/admin/service/user_service.py b/backend/app/admin/service/user_service.py index 8ffadeab..9392a2d0 100644 --- a/backend/app/admin/service/user_service.py +++ b/backend/app/admin/service/user_service.py @@ -212,7 +212,7 @@ class UserService: f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}', ] for prefix in key_prefix: - await redis_client.delete(prefix) + await redis_client.delete_prefix(prefix) return count @staticmethod @@ -314,6 +314,7 @@ class UserService: key_prefix = [ f'{settings.TOKEN_REDIS_PREFIX}:{user.id}', f'{settings.TOKEN_REFRESH_REDIS_PREFIX}:{user.id}', + f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}', ] for key in key_prefix: await redis_client.delete_prefix(key) diff --git a/backend/common/cache/__init__.py b/backend/common/cache/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/common/cache/decorator.py b/backend/common/cache/decorator.py new file mode 100644 index 00000000..f489931a --- /dev/null +++ b/backend/common/cache/decorator.py @@ -0,0 +1,226 @@ +import functools + +from collections.abc import Callable, Sequence +from typing import Any, ParamSpec, TypeVar + +from cachebox import make_hash_key +from msgspec import json + +from backend.common.cache.local import local_cache_manager +from backend.common.cache.pubsub import cache_pubsub_manager +from backend.common.context import ctx +from backend.common.exception import errors +from backend.common.log import log +from backend.core.conf import settings +from backend.database.redis import redis_client +from backend.utils.serializers import select_columns_serialize, select_list_serialize + +P = ParamSpec('P') +T = TypeVar('T') + +# 哈希缓存键排除参数 +_EXCLUDE_PARAMS = frozenset({'db', 'session', 'self', 'cls', 'request', 'response'}) + + +def build_cache_key( + name: str, + key: str | None, + key_builder: Callable[..., str] | None, + *args: Any, + **kwargs: Any, +) -> str: + """构建缓存 Key""" + if key_builder: + return f'{name}:{key_builder(*args, **kwargs)}' + + if key: + value = kwargs.get(key) + if value is None: + raise errors.ServerError(msg=f'缓存键构建失败,参数 "{key}" 不存在或值为空') + return f'{name}:{value}' + + filtered = {k: v for k, v in kwargs.items() if k not in _EXCLUDE_PARAMS and v is not None} + + if filtered: + hash_suffix = make_hash_key(*args, **kwargs) + return f'{name}:{hash_suffix}' + + return name + + +def user_key_builder() -> str: + """基于当前用户 ID 生成缓存 Key""" + user_id = ctx.user_id + if user_id is None: + raise errors.ServerError(msg='用户缓存键构建失败') + return str(user_id) + + +def _serialize_result(result: Any) -> bytes: + """ + 序列化缓存结果 + + :param result: 需要进行序列化的结果 + :return: + """ + # SQLAlchemy 查询表 + if hasattr(result, '__table__'): + return json.encode(select_columns_serialize(result)) + + # SQLAlchemy 查询列表 + if ( + isinstance(result, Sequence) + and not isinstance(result, (str, bytes)) + and len(result) > 0 + and hasattr(result[0], '__table__') + ): + return json.encode(select_list_serialize(result)) + + # 基本类型 + return json.encode(result) + + +def _deserialize_result(value: bytes) -> Any: + """ + 反序列化缓存结果 + + :param value: 缓存结果 + :return: + """ + try: + return json.decode(value) + except Exception: + return value + + +def cached( # noqa: C901 + name: str, + *, + key: str | None = None, + key_builder: Callable[..., str] | None = None, +) -> Callable[[Callable[P, T]], Callable[P, T]]: + """ + 缓存装饰器 + + :param name: 缓存名称(通常为缓存 Key 前缀) + :param key: 从方法参数中获取指定参数名的值作为缓存 Key,与 key_builder 互斥 + :param key_builder: 自定义 Key 生成函数,与 key 互斥 + :return: + """ + if key is not None and key_builder is not None: + raise errors.ServerError(msg='缓存 key 和 key_builder 不能同时使用') + + def decorator(func: Callable[P, T]) -> Callable[P, T]: # noqa: C901 + @functools.wraps(func) + async def wrapper(*args: P.args, **kwargs: P.kwargs) -> T: + cache_key = build_cache_key(name, key, key_builder, *args, **kwargs) + + # L1: 本地缓存 + if settings.CACHE_LOCAL_ENABLED: + local_value = local_cache_manager.get(cache_key) + if local_value is not None: + return local_value + + # L2: Redis 缓存 + try: + redis_value = await redis_client.get(cache_key) + if redis_value is not None: + result = _deserialize_result(redis_value) + # 回填 L1 + if settings.CACHE_LOCAL_ENABLED: + local_cache_manager.set(cache_key, result) + return result + except Exception as e: + log.warning(f'[Cache] GET error: {e}') + + # 缓存未命中 + result = await func(*args, **kwargs) + + if result is not None: + try: + # 回填 L1 + if settings.CACHE_LOCAL_ENABLED: + local_cache_manager.set(cache_key, result) + + # 回填 L2 + serialized_result = _serialize_result(result) + if settings.CACHE_REDIS_TTL: + await redis_client.setex(cache_key, settings.CACHE_REDIS_TTL, serialized_result) + else: + await redis_client.set(cache_key, serialized_result) + except Exception as e: + log.warning(f'[Cache] SET error: {e}') + + return result + + return wrapper + + return decorator + + +def cache_invalidate( # noqa: C901 + name: str, + *, + key: str | None = None, + key_builder: Callable[..., str] | None = None, + atomic: bool = True, +) -> Callable[[Callable[P, T]], Callable[P, T]]: + """ + 缓存失效装饰器 + + :param name: 缓存名称(通常为缓存 Key 前缀) + :param key: 从方法参数中获取指定参数名的值作为缓存 Key,与 key_builder 互斥 + :param key_builder: 自定义 Key 生成函数,与 key 互斥 + :param atomic: 是否保证缓存原子性 + :return: + """ + if key is not None and key_builder is not None: + raise errors.ServerError(msg='缓存 key 和 key_builder 不能同时使用') + + def decorator(func: Callable[P, T]) -> Callable[P, T]: + @functools.wraps(func) + async def wrapper(*args: P.args, **kwargs: P.kwargs) -> T: + result = await func(*args, **kwargs) + + # 尝试失效缓存 + invalidate_success = False + invalidate_error = None + + try: + invalidate_key = build_cache_key(name, key, key_builder, *args, **kwargs) + + # L2 缓存失效 + if invalidate_key == name: + await redis_client.delete(invalidate_key) + else: + await redis_client.delete_prefix(invalidate_key) + + # L1 缓存失效 + if settings.CACHE_LOCAL_ENABLED: + if invalidate_key == name: + local_cache_manager.delete(invalidate_key) + else: + local_cache_manager.delete_prefix(invalidate_key) + + # 广播失效消息(通知其他节点清除本地缓存) + if settings.CACHE_LOCAL_ENABLED: + if invalidate_key == name: + await cache_pubsub_manager.publish_invalidation(invalidate_key) + else: + await cache_pubsub_manager.publish_invalidation(invalidate_key, is_delete_prefix=True) + + except Exception as e: + log.error(f'[Cache] INVALIDATE error: {e}') + invalidate_error = e + else: + invalidate_success = True + + # 原子性检查 + if atomic and not invalidate_success: + raise errors.ServerError(msg='缓存失效失败,数据可能不一致', data=invalidate_error) + + return result + + return wrapper + + return decorator diff --git a/backend/common/cache/local.py b/backend/common/cache/local.py new file mode 100644 index 00000000..4292d589 --- /dev/null +++ b/backend/common/cache/local.py @@ -0,0 +1,56 @@ +from typing import Any + +import cachebox + +from backend.core.conf import settings + + +class LocalCacheManager: + """本地缓存管理器""" + + def __init__(self) -> None: + self.hot_cache: cachebox.TTLCache = cachebox.TTLCache( + settings.CACHE_LOCAL_MAXSIZE, ttl=settings.CACHE_LOCAL_TTL + ) + + def get(self, key: str) -> Any: + """获取缓存""" + try: + return self.hot_cache[key] + except KeyError: + return None + + def set(self, key: str, value: Any) -> None: + """设置缓存""" + self.hot_cache[key] = value + + def delete(self, key: str) -> bool: + """删除缓存""" + try: + del self.hot_cache[key] + except KeyError: + return False + return True + + def clear(self) -> None: + """清空缓存""" + self.hot_cache.clear() + + def delete_prefix(self, prefix: str, exclude: str | list[str] | None = None) -> None: + """ + 删除指定前缀的缓存 + + :param prefix: 要删除的键前缀 + :param exclude: 要排除的键或键列表 + :return: + """ + exclude_set = set(exclude) if isinstance(exclude, list) else {exclude} if isinstance(exclude, str) else set() + for key in list(self.hot_cache.keys()): + if key.startswith(prefix) and key not in exclude_set: + try: + del self.hot_cache[key] + except KeyError: + pass + + +local_cache_manager = LocalCacheManager() diff --git a/backend/common/cache/pubsub.py b/backend/common/cache/pubsub.py new file mode 100644 index 00000000..0f1a6e6f --- /dev/null +++ b/backend/common/cache/pubsub.py @@ -0,0 +1,112 @@ +import asyncio +import json + +from backend.common.cache.local import local_cache_manager +from backend.common.log import log +from backend.core.conf import settings +from backend.database.redis import RedisCli, redis_client + + +class CachePubSubManager: + """缓存 Pub/Sub 管理器""" + + _pubsub_task: asyncio.Task | None = None + + @staticmethod + async def publish_invalidation(key: str, *, is_delete_prefix: bool) -> None: + """ + 发布缓存失效通知 + + :param key: 缓存键 + :param is_delete_prefix: 是否删除符合前缀的所有缓存 + :return: + """ + try: + message = json.dumps({'key': key, 'is_delete_prefix': is_delete_prefix}) + await redis_client.publish(settings.CACHE_PUBSUB_CHANNEL, message) + except Exception as e: + log.warning(f'[CachePubSub] 发布通知失败: {e}') + + @staticmethod + async def subscribe_and_listen() -> None: # noqa: C901 + """订阅并监听缓存失效通知""" + reconnect_attempts = 0 + + while reconnect_attempts < settings.CACHE_PUBSUB_MAX_RECONNECT_ATTEMPTS: + pubsub_client: RedisCli | None = None + pubsub = None + + try: + # 使用独立连接 + pubsub_client = RedisCli() + pubsub = pubsub_client.pubsub() + await pubsub.subscribe(settings.CACHE_PUBSUB_CHANNEL) + + # 发布订阅成功 + reconnect_attempts = 0 + + async for message in pubsub.listen(): + if message['type'] == 'message': + try: + data = json.loads(message['data']) + key = data['key'] + if not data['is_delete_prefix']: + local_cache_manager.delete(key) + else: + local_cache_manager.delete_prefix(key) + except json.JSONDecodeError as e: + log.warning(f'[CachePubSub] 消息格式错误 {e}') + except Exception as e: + log.error(f'[CachePubSub] 处理通知失败: {e}') + + except asyncio.CancelledError: + break + except Exception as e: + reconnect_attempts += 1 + log.error( + f'[CachePubSub] 订阅异常 ({reconnect_attempts}/{settings.CACHE_PUBSUB_MAX_RECONNECT_ATTEMPTS}): {e}' + ) + + if reconnect_attempts >= settings.CACHE_PUBSUB_MAX_RECONNECT_ATTEMPTS: + log.error('[CachePubSub] 达到最大重连次数,停止订阅') + break + + await asyncio.sleep(settings.CACHE_PUBSUB_RECONNECT_DELAY) + finally: + if pubsub_client: + try: + await pubsub_client.aclose() + except Exception: + pass + if pubsub: + try: + await pubsub.aclose() + except Exception: + pass + + @classmethod + def start_listener(cls) -> None: + """启动缓存 Pub/Sub 监听器""" + if not settings.CACHE_LOCAL_ENABLED: + return + + if cls._pubsub_task is None or cls._pubsub_task.done(): + cls._pubsub_task = asyncio.create_task(cls.subscribe_and_listen()) + + @classmethod + async def stop_listener(cls) -> None: + """停止缓存 Pub/Sub 监听器""" + if cls._pubsub_task is None: + return + + if not cls._pubsub_task.done(): + cls._pubsub_task.cancel() + try: + await cls._pubsub_task + except asyncio.CancelledError: + pass + + cls._pubsub_task = None + + +cache_pubsub_manager = CachePubSubManager() diff --git a/backend/common/cache/warmup.py b/backend/common/cache/warmup.py new file mode 100644 index 00000000..5970a4d3 --- /dev/null +++ b/backend/common/cache/warmup.py @@ -0,0 +1,36 @@ +from backend.common.log import log +from backend.database.db import async_db_session +from backend.plugin.config.enums import ConfigType + + +async def cache_warmup() -> None: + """缓存预热""" + await _warmup_config() + await _warmup_dict() + + +async def _warmup_config() -> None: + """预热参数配置缓存""" + try: + from backend.plugin.config.service.config_service import config_service + + async with async_db_session() as db: + for type in ConfigType.get_member_values(): + await config_service.get_all(db=db, type=type) + except ImportError: + pass + except Exception as e: + log.warning(f'[Warmup] 参数配置缓存预热失败: {e}') + + +async def _warmup_dict() -> None: + """预热数据字典缓存""" + try: + from backend.plugin.dict.service.dict_data_service import dict_data_service + + async with async_db_session() as db: + await dict_data_service.get_all(db=db) + except ImportError: + pass + except Exception as e: + log.warning(f'[Warmup] 数据字典缓存预热失败: {e}') diff --git a/backend/common/context.py b/backend/common/context.py index 834fcee5..2b06d0a4 100644 --- a/backend/common/context.py +++ b/backend/common/context.py @@ -21,6 +21,8 @@ class TypedContextProtocol(Protocol): permission: str | None language: str + user_id: int | None + class TypedContext(TypedContextProtocol, _Context): def __getattr__(self, name: str) -> Any: diff --git a/backend/common/security/permission.py b/backend/common/security/permission.py index ae6e45bf..46168d8c 100644 --- a/backend/common/security/permission.py +++ b/backend/common/security/permission.py @@ -40,7 +40,8 @@ class RequestPermission: if settings.RBAC_ROLE_MENU_MODE: if not isinstance(self.value, str): raise errors.ServerError - # 附加权限标识到请求状态 + + # 设置权限标识到上下文 ctx.permission = self.value diff --git a/backend/core/conf.py b/backend/core/conf.py index 8e9e1ccf..510daa9a 100644 --- a/backend/core/conf.py +++ b/backend/core/conf.py @@ -1,6 +1,6 @@ import shutil -from functools import lru_cache +from functools import cache from re import Pattern from typing import Any, Literal @@ -68,6 +68,17 @@ class Settings(BaseSettings): # Redis REDIS_TIMEOUT: int = 5 + # 缓存 + CACHE_LOCAL_ENABLED: bool = True + CACHE_LOCAL_MAXSIZE: int = 100000 + CACHE_LOCAL_TTL: int = 60 * 60 * 2 # 2 小时 + CACHE_REDIS_TTL: int = 60 * 60 * 2 # 2 小时 + CACHE_CONFIG_REDIS_PREFIX: str = 'fba:cache:config' + CACHE_DICT_REDIS_PREFIX: str = 'fba:cache:dict' + CACHE_PUBSUB_CHANNEL: str = 'fba:cache:invalidate' + CACHE_PUBSUB_RECONNECT_DELAY: int = 5 # 重连延迟(秒) + CACHE_PUBSUB_MAX_RECONNECT_ATTEMPTS: int = 10 # 最大重连次数 + # .env Snowflake SNOWFLAKE_DATACENTER_ID: int | None = None SNOWFLAKE_WORKER_ID: int | None = None @@ -213,6 +224,7 @@ class Settings(BaseSettings): 'new_password', 'confirm_password', ] + OPERA_LOG_QUEUE_MAXSIZE: int = 100000 OPERA_LOG_QUEUE_BATCH_CONSUME_SIZE: int = 100 OPERA_LOG_QUEUE_TIMEOUT: int = 60 # 1 分钟 @@ -303,7 +315,7 @@ class Settings(BaseSettings): return values -@lru_cache +@cache def get_settings() -> Settings: """获取全局配置单例""" if not ENV_FILE_PATH.exists(): diff --git a/backend/core/registrar.py b/backend/core/registrar.py index 1a11d81f..78c33d4a 100644 --- a/backend/core/registrar.py +++ b/backend/core/registrar.py @@ -17,6 +17,8 @@ from starlette_context.middleware import ContextMiddleware from starlette_context.plugins import RequestIdPlugin from backend import __version__ +from backend.common.cache.pubsub import cache_pubsub_manager +from backend.common.cache.warmup import cache_warmup from backend.common.exception.exception_handler import register_exception from backend.common.log import set_custom_logfile, setup_logging from backend.common.response.response_code import StandardResponseCode @@ -66,8 +68,17 @@ async def register_init(app: FastAPI) -> AsyncGenerator[None, None]: # 创建操作日志任务 create_task(OperaLogMiddleware.consumer()) + # 缓存预热 + await cache_warmup() + + # 启动缓存 Pub/Sub 监听器 + cache_pubsub_manager.start_listener() + yield + # 停止缓存 Pub/Sub 监听器 + await cache_pubsub_manager.stop_listener() + # 释放 snowflake 节点 await snowflake.shutdown() diff --git a/backend/middleware/jwt_auth_middleware.py b/backend/middleware/jwt_auth_middleware.py index 081f8675..4e90829b 100644 --- a/backend/middleware/jwt_auth_middleware.py +++ b/backend/middleware/jwt_auth_middleware.py @@ -7,6 +7,7 @@ from starlette.authentication import AuthenticationError as StarletteAuthenticat from starlette.requests import HTTPConnection from backend.app.admin.schema.user import GetUserInfoWithRelationDetail +from backend.common.context import ctx from backend.common.exception.errors import TokenError from backend.common.log import log from backend.common.security.jwt import jwt_authentication @@ -95,6 +96,9 @@ class JwtAuthMiddleware(AuthenticationBackend): log.exception(f'JWT 授权异常:{e}') raise AuthenticationError(code=getattr(e, 'code', 500), msg=getattr(e, 'msg', 'Internal Server Error')) + # 设置用户 ID 到上下文 + ctx.user_id = user.id + # 请注意,此返回使用非标准模式,所以在认证通过时,将丢失某些标准特性 # 标准返回模式请查看:https://www.starlette.io/authentication/ return AuthCredentials(['authenticated']), user diff --git a/backend/middleware/opera_log_middleware.py b/backend/middleware/opera_log_middleware.py index b0556861..65730afa 100644 --- a/backend/middleware/opera_log_middleware.py +++ b/backend/middleware/opera_log_middleware.py @@ -31,7 +31,7 @@ from backend.utils.trace_id import get_request_trace_id class OperaLogMiddleware(BaseHTTPMiddleware): """操作日志中间件""" - opera_log_queue: Queue = Queue(maxsize=100000) + opera_log_queue: Queue = Queue(maxsize=settings.OPERA_LOG_QUEUE_MAXSIZE) async def dispatch(self, request: Request, call_next: Any) -> Response: """ diff --git a/backend/plugin/config/service/config_service.py b/backend/plugin/config/service/config_service.py index 23c3e034..f69d37a3 100644 --- a/backend/plugin/config/service/config_service.py +++ b/backend/plugin/config/service/config_service.py @@ -3,8 +3,10 @@ from typing import Any from sqlalchemy.ext.asyncio import AsyncSession +from backend.common.cache.decorator import cache_invalidate, cached from backend.common.exception import errors from backend.common.pagination import paging_data +from backend.core.conf import settings from backend.plugin.config.crud.crud_config import config_dao from backend.plugin.config.model import Config from backend.plugin.config.schema.config import ( @@ -26,13 +28,16 @@ class ConfigService: :param pk: 参数配置 ID :return: """ - config = await config_dao.get(db, pk) if not config: raise errors.NotFoundError(msg='参数配置不存在') return config @staticmethod + @cached( + settings.CACHE_CONFIG_REDIS_PREFIX, + key_builder=lambda *, db, type: f'type:{type}', + ) async def get_all(*, db: AsyncSession, type: str | None) -> Sequence[Config | None]: """ 获取所有参数配置 @@ -41,7 +46,6 @@ class ConfigService: :param type: 参数配置类型 :return: """ - return await config_dao.get_all(db, type) @staticmethod @@ -58,6 +62,7 @@ class ConfigService: return await paging_data(db, config_select) @staticmethod + @cache_invalidate(settings.CACHE_CONFIG_REDIS_PREFIX) async def create(*, db: AsyncSession, obj: CreateConfigParam) -> None: """ 创建参数配置 @@ -66,13 +71,13 @@ class ConfigService: :param obj: 参数配置创建参数 :return: """ - config = await config_dao.get_by_key(db, obj.key) if config: raise errors.ConflictError(msg=f'参数配置 {obj.key} 已存在') await config_dao.create(db, obj) @staticmethod + @cache_invalidate(settings.CACHE_CONFIG_REDIS_PREFIX) async def update(*, db: AsyncSession, pk: int, obj: UpdateConfigParam) -> int: """ 更新参数配置 @@ -82,7 +87,6 @@ class ConfigService: :param obj: 参数配置更新参数 :return: """ - config = await config_dao.get(db, pk) if not config: raise errors.NotFoundError(msg='参数配置不存在') @@ -94,6 +98,7 @@ class ConfigService: return count @staticmethod + @cache_invalidate(settings.CACHE_CONFIG_REDIS_PREFIX) async def bulk_update(*, db: AsyncSession, objs: list[UpdateConfigsParam]) -> int: """ 批量更新参数配置 @@ -102,7 +107,6 @@ class ConfigService: :param objs: 参数配置批量更新参数 :return: """ - for _batch in range(0, len(objs), 1000): for obj in objs: config = await config_dao.get(db, obj.id) @@ -116,6 +120,7 @@ class ConfigService: return count @staticmethod + @cache_invalidate(settings.CACHE_CONFIG_REDIS_PREFIX) async def delete(*, db: AsyncSession, pks: list[int]) -> int: """ 批量删除参数配置 @@ -124,7 +129,6 @@ class ConfigService: :param pks: 参数配置 ID 列表 :return: """ - count = await config_dao.delete(db, pks) return count diff --git a/backend/plugin/core.py b/backend/plugin/core.py index 1576b6ab..0a7ef01f 100644 --- a/backend/plugin/core.py +++ b/backend/plugin/core.py @@ -22,8 +22,8 @@ from backend.utils.async_helper import run_await from backend.utils.dynamic_import import get_model_objects, import_module_cached -@lru_cache -def get_plugins() -> list[str]: +@lru_cache(maxsize=128) +def get_plugins() -> tuple[str, ...]: """获取插件列表""" plugin_packages = [] @@ -37,7 +37,7 @@ def get_plugins() -> list[str]: if os.path.isdir(item_path) and '__init__.py' in os.listdir(item_path): plugin_packages.append(item) - return plugin_packages + return tuple(plugin_packages) def get_plugin_models() -> list[object]: @@ -104,7 +104,7 @@ def parse_plugin_config() -> tuple[list[dict[str, Any]], list[dict[str, Any]]]: plugins = get_plugins() - # 使用独立单例,避免与主线程冲突 + # 使用独立连接 current_redis_client = RedisCli() run_await(current_redis_client.init)() diff --git a/backend/plugin/dict/service/dict_data_service.py b/backend/plugin/dict/service/dict_data_service.py index 6b0a1f44..10ea2442 100644 --- a/backend/plugin/dict/service/dict_data_service.py +++ b/backend/plugin/dict/service/dict_data_service.py @@ -3,8 +3,10 @@ from typing import Any from sqlalchemy.ext.asyncio import AsyncSession +from backend.common.cache.decorator import cache_invalidate, cached from backend.common.exception import errors from backend.common.pagination import paging_data +from backend.core.conf import settings from backend.plugin.dict.crud.crud_dict_data import dict_data_dao from backend.plugin.dict.crud.crud_dict_type import dict_type_dao from backend.plugin.dict.model import DictData @@ -23,13 +25,16 @@ class DictDataService: :param pk: 字典数据 ID :return: """ - dict_data = await dict_data_dao.get(db, pk) if not dict_data: raise errors.NotFoundError(msg='字典数据不存在') return dict_data @staticmethod + @cached( + settings.CACHE_DICT_REDIS_PREFIX, + key_builder=lambda *, db, code: f'type:{code}', + ) async def get_by_type_code(*, db: AsyncSession, code: str) -> Sequence[DictData]: """ 获取字典数据详情 @@ -38,13 +43,16 @@ class DictDataService: :param code: 字典类型编码 :return: """ - dict_datas = await dict_data_dao.get_by_type_code(db, code) if not dict_datas: raise errors.NotFoundError(msg='字典数据不存在') return dict_datas @staticmethod + @cached( + settings.CACHE_DICT_REDIS_PREFIX, + key_builder=lambda *, db: 'all', + ) async def get_all(*, db: AsyncSession) -> Sequence[DictData]: """ 获取所有字典数据 @@ -86,6 +94,7 @@ class DictDataService: return await paging_data(db, dict_data_select) @staticmethod + @cache_invalidate(settings.CACHE_DICT_REDIS_PREFIX) async def create(*, db: AsyncSession, obj: CreateDictDataParam) -> None: """ 创建字典数据 @@ -103,6 +112,7 @@ class DictDataService: await dict_data_dao.create(db, obj, dict_type.code) @staticmethod + @cache_invalidate(settings.CACHE_DICT_REDIS_PREFIX) async def update(*, db: AsyncSession, pk: int, obj: UpdateDictDataParam) -> int: """ 更新字典数据 @@ -112,7 +122,6 @@ class DictDataService: :param obj: 字典数据更新参数 :return: """ - dict_data = await dict_data_dao.get(db, pk) if not dict_data: raise errors.NotFoundError(msg='字典数据不存在') @@ -127,6 +136,7 @@ class DictDataService: return count @staticmethod + @cache_invalidate(settings.CACHE_DICT_REDIS_PREFIX) async def delete(*, db: AsyncSession, obj: DeleteDictDataParam) -> int: """ 批量删除字典数据 @@ -135,7 +145,6 @@ class DictDataService: :param obj: 字典数据 ID 列表 :return: """ - count = await dict_data_dao.delete(db, obj.pks) return count diff --git a/backend/plugin/installer.py b/backend/plugin/installer.py index 01f58dbc..5a615b72 100644 --- a/backend/plugin/installer.py +++ b/backend/plugin/installer.py @@ -104,7 +104,7 @@ async def install_zip_plugin(file: UploadFile | str) -> str: await _append_env_example(full_plugin_path) await install_requirements_async(plugin_dir_name) - await redis_client.set(f'{settings.PLUGIN_REDIS_PREFIX}:changed', 'ture') + await redis_client.set(f'{settings.PLUGIN_REDIS_PREFIX}:changed', 'true') return plugin_name @@ -133,6 +133,6 @@ async def install_git_plugin(repo_url: str) -> str: await _append_env_example(path) await install_requirements_async(repo_name) - await redis_client.set(f'{settings.PLUGIN_REDIS_PREFIX}:changed', 'ture') + await redis_client.set(f'{settings.PLUGIN_REDIS_PREFIX}:changed', 'true') return repo_name diff --git a/backend/plugin/requirements.py b/backend/plugin/requirements.py index 52f3cce7..299c0d27 100644 --- a/backend/plugin/requirements.py +++ b/backend/plugin/requirements.py @@ -9,20 +9,10 @@ from starlette.concurrency import run_in_threadpool from backend.core.conf import settings from backend.core.path_conf import PLUGIN_DIR +from backend.plugin.core import get_plugins from backend.plugin.errors import PluginInstallError -def get_plugins() -> list[str]: - """ - 获取插件列表 - - 注意:此函数从 backend.plugin.core 导入以避免循环依赖 - """ - from backend.plugin.core import get_plugins as _get_plugins - - return _get_plugins() - - def _is_in_virtualenv() -> bool: """检测当前是否在虚拟环境中运行""" return hasattr(sys, 'real_prefix') or (hasattr(sys, 'base_prefix') and sys.base_prefix != sys.prefix) diff --git a/backend/utils/dynamic_import.py b/backend/utils/dynamic_import.py index 08aee91e..e8885091 100644 --- a/backend/utils/dynamic_import.py +++ b/backend/utils/dynamic_import.py @@ -13,7 +13,7 @@ from backend.common.log import log T = TypeVar('T') -@lru_cache(maxsize=512) +@lru_cache(maxsize=128) def import_module_cached(module_path: str) -> Any: """ 缓存导入模块 @@ -84,9 +84,9 @@ def get_app_models() -> list[object]: return objs -@lru_cache -def get_all_models() -> list[object]: +@lru_cache(256) +def get_all_models() -> tuple[object, ...]: """获取所有模型类""" from backend.plugin.core import get_plugin_models - return get_app_models() + get_plugin_models() + return tuple(get_app_models() + get_plugin_models()) diff --git a/pyproject.toml b/pyproject.toml index d87dea2f..d3263c0b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,6 +14,8 @@ dependencies = [ "asyncmy>=0.2.11", "asyncpg>=0.31.0", "bcrypt>=5.0.0", + # If there is a serious problem, we will try `moka-py` + "cachebox>=5.2.2", "cappa>=0.31.0", "celery>=5.6.2", # When celery version < 6.0.0 diff --git a/requirements.txt b/requirements.txt index ee316ffa..01886eea 100644 --- a/requirements.txt +++ b/requirements.txt @@ -32,6 +32,8 @@ bidict==0.23.1 # via python-socketio billiard==4.2.4 # via celery +cachebox==5.2.2 + # via fastapi-best-architecture cappa==0.31.0 # via fastapi-best-architecture celery==5.6.2 diff --git a/uv.lock b/uv.lock index 2df495fb..828c7e1e 100644 --- a/uv.lock +++ b/uv.lock @@ -300,6 +300,125 @@ wheels = [ { url = "https://mirrors.aliyun.com/pypi/packages/cb/87/8bab77b323f16d67be364031220069f79159117dd5e43eeb4be2fef1ac9b/billiard-4.2.4-py3-none-any.whl", hash = "sha256:525b42bdec68d2b983347ac312f892db930858495db601b5836ac24e6477cde5" }, ] +[[package]] +name = "cachebox" +version = "5.2.2" +source = { registry = "https://mirrors.aliyun.com/pypi/simple" } +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/ab/04/0a0e6517ce0b4805dbd4a2a07ab679fb862938f841ea365403c7e51298dc/cachebox-5.2.2.tar.gz", hash = "sha256:a140ea693faf2c9aa9fccdf80bed9912f8f3eba6dfb74f921a34877fcb93a902" } +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/60/bb/5763483188883295bf0adfc915f77528f3f1d21884f6fdef2aa4d04e6815/cachebox-5.2.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:10c8d7870727f19215c826ddfd30a6af2639749dca27e092f28bd83e4e8dc594" }, + { url = "https://mirrors.aliyun.com/pypi/packages/5a/17/ef5c90fa30e2166a396f22308a061dc3a9c023702b6fe50c5ac5d00c3696/cachebox-5.2.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3e3871289dc57d6e480a3b80afc0d1be3c259b32047132bf469cce1b63f28434" }, + { url = "https://mirrors.aliyun.com/pypi/packages/0c/be/efb1cd6aad77670ae4e2643ad368fb8ef5714f6b0237efea4f97760aa555/cachebox-5.2.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ff7684056057459421c31bf25b90eb5317050ba077de10508b4bdc475ed92f02" }, + { url = "https://mirrors.aliyun.com/pypi/packages/94/60/bdec91c8717d9ffa4a2acf0e3b1b72268758cbabedc2d51693cbf6092011/cachebox-5.2.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9592046f3217cc6696903950b22529e80c50de2499c0137301b98e2a1c9b5206" }, + { url = "https://mirrors.aliyun.com/pypi/packages/75/7a/b88ac43a45457466e0a76414cf5bd903fccc16afc448bd311fdd5d30c997/cachebox-5.2.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bc31b2c74526b8e7b181b7fa6b8e67fd1ea3f35c9c8e004a68b199fb8f67f4f7" }, + { url = "https://mirrors.aliyun.com/pypi/packages/2a/f4/b310c1b424633b9084db84abd3b2d0a8c0688e2b00ad78149eaa15b42e0b/cachebox-5.2.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f904b596ec206fed7210fb77247a79109d5ced8df755e45e580b82461b6f83bd" }, + { url = "https://mirrors.aliyun.com/pypi/packages/b7/50/38e082b0c89650dd886a30d4ca1f1f2acc345a061752e1a4ef643dcb07b0/cachebox-5.2.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:800259e6f83fbcfe624a40afd55e1935c1edc767b636fdc468d7aca3a95f0eb2" }, + { url = "https://mirrors.aliyun.com/pypi/packages/77/b4/dcf0e8be221d1f2e352ff0b0007b86ab9ff4c221b12396ac60bb90f15056/cachebox-5.2.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4919566dacd813296a07a589c86f078e3cbc9330ef504c4005617ff709c3987c" }, + { url = "https://mirrors.aliyun.com/pypi/packages/fb/f9/b925fb94f6b2d0aba096e7c95fde94860af6acf5bd7ec972fa91918afbfa/cachebox-5.2.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7e6e8ffb8fd82083ca8cb71faee2d14d15889e428c88562a50a89eb13bb560d5" }, + { url = "https://mirrors.aliyun.com/pypi/packages/1b/78/ed1e7cc2fbdcebe88866701491e656c906bc93719587cb4aeb41ea57a372/cachebox-5.2.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:87e75a5cd4e37c53f25c5986ca8125ce7555731a08a9b99faaf32289d3f3e2c3" }, + { url = "https://mirrors.aliyun.com/pypi/packages/20/c8/8eaf0227ac4596846fba907d68a980598e38abc03567e611e45d9594c96f/cachebox-5.2.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e4fbda452f106e5be7ab9b8aed2102a0d4fcf7d8aa077d77066bf6da9ec3430f" }, + { url = "https://mirrors.aliyun.com/pypi/packages/7b/b7/851f0189eaf2d9fa09f4245701ac8e558753f3b35f982492fa9f92572543/cachebox-5.2.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c9bd1bfac007027043c3b89c82600641cd8f1a74ead0f58862e6e3c9ea5448f4" }, + { url = "https://mirrors.aliyun.com/pypi/packages/59/cb/2f7a8cb1502f73e6b58501ff9ebedd7e7a0c3858b7efc656d1dabdc17266/cachebox-5.2.2-cp310-cp310-win32.whl", hash = "sha256:884eeccf106a94b17ba2484c8f3b8edb48847b6eec4beb250c29775fe859036a" }, + { url = "https://mirrors.aliyun.com/pypi/packages/1c/0d/8dfab8cfa64caf862ba2efefe6565093193db9b4da6a8fe6c6e529c8651c/cachebox-5.2.2-cp310-cp310-win_amd64.whl", hash = "sha256:d6a3b9cfb3fb055fa5652eaccd9401889d177470c29ff8867c9f0d68859843cd" }, + { url = "https://mirrors.aliyun.com/pypi/packages/7d/b6/36461432b5c4ba65523c3a1ecc82f1f464b8aeeb061fbbf115013a0ab7bf/cachebox-5.2.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:011783c98e15e6c4e1632c1868a11edba4f295b51174b31b1e64b2baec3d2cfc" }, + { url = "https://mirrors.aliyun.com/pypi/packages/32/a2/a8f98f653f8a6b02e7cc4e44e28dbf6c94952c24548d5676d3c6bad904e8/cachebox-5.2.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:04f71e0e252e50e1d1608f23a9d25709f64d05496cb32ea6f8c90c09ad2f73ce" }, + { url = "https://mirrors.aliyun.com/pypi/packages/af/e7/898e180425470133a9f4c6257c3b97be69baaf2e329fc74e221a571a9013/cachebox-5.2.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f08b67b0e0c195f0553d80e5282750a9f6ff115249ed2762bfda95e280267e23" }, + { url = "https://mirrors.aliyun.com/pypi/packages/84/0c/f7e65ef449cacb66df556a34863f76e64daf9e6a52b80805811bc2fd3d95/cachebox-5.2.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:84189dba82519c14ea816f94bbe3bc59a344670c54b898ce8875e68dc5814614" }, + { url = "https://mirrors.aliyun.com/pypi/packages/f3/44/5b5164de26d691f65dfb25e0b2e5625b2b31bf09770470b935c4bc9e3c9a/cachebox-5.2.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d5532ebd6d4f7e22c3230959bf7063955b05e2921ed019e512cebc88ed4b33b1" }, + { url = "https://mirrors.aliyun.com/pypi/packages/98/d0/69a35173f6bb5387a9aa6c04cf57f78a3b1f901eb4304c7cf8ab48f57870/cachebox-5.2.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1931b1cf538788d76166b169da9f98092705d0028a7692a9e4bebd37af477bb4" }, + { url = "https://mirrors.aliyun.com/pypi/packages/15/2c/49361106c390d597f6a24a26eee89063aa51f4867b77b8d584388fc9a259/cachebox-5.2.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:41f020992ea81b8bdbc1d240e9c7e34301e222507de7699748c8d17e0b2deb78" }, + { url = "https://mirrors.aliyun.com/pypi/packages/f3/23/50002598686143134e17ea69414d2bd357c2c5854e1ce493fb705b6fae75/cachebox-5.2.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:97cdb08aca071e1f581bc1c8995c6b10fe6d8a480b8d4ccbe2104eb7846b9d3c" }, + { url = "https://mirrors.aliyun.com/pypi/packages/64/af/e79ebec77002550754c6e66602a1b5e339963aa9d85294b60aa787483d3d/cachebox-5.2.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3bbb7512f2a76b6375200450fa5fd94f88c4f1d15fc434a4a9450782493f8ad0" }, + { url = "https://mirrors.aliyun.com/pypi/packages/20/3c/464cc2a16428c66100bf9513cc68aa8aa5ec10f235f9ce3e0174f64162a5/cachebox-5.2.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:390a50c32d1942ef581dd728053663d2dbe3e4c066458496ec5b791ffd3d18f7" }, + { url = "https://mirrors.aliyun.com/pypi/packages/bf/f7/c51f1a260d5e26d651007b66350663f881501d6501193ddc1e6826a3a300/cachebox-5.2.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:a70d3a19289f4748d93fc149bc103ffc81a54af3a4aa2e9730a0ae3217af7071" }, + { url = "https://mirrors.aliyun.com/pypi/packages/65/e0/c9ae4944fc8f7e0efe6ec9eaf25c875da795985c1896ac5ca2f1743ba874/cachebox-5.2.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:67c45c2b1e3acf0630fd5c23cdb83b2742ae6a7098ac4758fc5f596c0d6c6c8c" }, + { url = "https://mirrors.aliyun.com/pypi/packages/7a/9d/1b0abf5772ebac0fbb7812b26412f5cf25930ca8137ae48312e212648dd7/cachebox-5.2.2-cp311-cp311-win32.whl", hash = "sha256:3f05fc1477c91406f9c8f2affa6535ffd6bc6bb23f84f56c8f87da6eed4aeff0" }, + { url = "https://mirrors.aliyun.com/pypi/packages/5d/d5/2fa6ec474e7d294c756b794802573f6a66f96cac09187bb461d24d9915c8/cachebox-5.2.2-cp311-cp311-win_amd64.whl", hash = "sha256:455edd8b4c01c6232259a1177feb553ff0541582fbed394dd0a5f30b3a193e4b" }, + { url = "https://mirrors.aliyun.com/pypi/packages/5b/ea/f7c214049f87ca43f9022ba64d90063b64af3ece2f80df95880700544249/cachebox-5.2.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:8f52a2d7902ca65a5c2c824a038a09c924a4322d2792d87178d5c49aec857598" }, + { url = "https://mirrors.aliyun.com/pypi/packages/97/3e/77505237cc325a1ab6a058be8b4444716e48e0a89ac57eab4f521ef80f04/cachebox-5.2.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0e419dc10599f65e9f6115319fd66517e639e6bd6bf40cdfd0190ac3360663ed" }, + { url = "https://mirrors.aliyun.com/pypi/packages/0c/b2/99792ee9bdf2a3eeb85d30e7257a1b69a0f852b2b437979b70d582f31c92/cachebox-5.2.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:803be9146e51d904dbbd8ca104fe366754c77d7f0bb60012d332c194fad7173e" }, + { url = "https://mirrors.aliyun.com/pypi/packages/41/e8/9d61183e9286bbe8bfa7a9b40432384bda7e3696f74e1d658cfba111c33a/cachebox-5.2.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:33157e5c132f3919495071c506f574d66d96f8a21456f998f60be10c2f92e25f" }, + { url = "https://mirrors.aliyun.com/pypi/packages/cd/90/c5aff2efa8b5df445f79b8589e22221de5a5fce1c5e7f475346f3c27e271/cachebox-5.2.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05ea02d6745223c33b04f48d801aec449118f085f77f615b8f0a7fb00c121c2d" }, + { url = "https://mirrors.aliyun.com/pypi/packages/a7/03/2727f6b383d0838704460cbcf964bec69311acbbe8f6b90344872b61474b/cachebox-5.2.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ecacc803639c4bc38ccde3983b1b021ca11eb472271b45187e310c7e363b482f" }, + { url = "https://mirrors.aliyun.com/pypi/packages/bf/ca/37a9cc44981d0e810af69562f432eee2ac2bdcb4a33d9a2484cb755ce0a7/cachebox-5.2.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ad685fb02bd63aa09d031af8507cee3bb308d6c7486c7fa8b4e28b310be6555" }, + { url = "https://mirrors.aliyun.com/pypi/packages/f7/67/ec6e4a32caf1aef79aa976edc5104c354411e33aa8699e53bb40f7c6a607/cachebox-5.2.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7a95936bedace3878a200700c00d5a7c0f0afdbc85d1ecc7d1fb47da6d7617bc" }, + { url = "https://mirrors.aliyun.com/pypi/packages/e8/ad/96c259366eed4108cc72bde38f5cff24fcc85b6dca2eb1f2d294918237ff/cachebox-5.2.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:715eb907d8d595bc83a62250b84aa21e8794a9644b92c9eb254a7a50a56e3d5e" }, + { url = "https://mirrors.aliyun.com/pypi/packages/61/90/98c1cf839e3ab3ed97f83da87b2041eaa8b912068c0bd000306687b7d26b/cachebox-5.2.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:17b54f13b55e72e67aa531e0c805b2cc9a42ac746937c2df22bcdef21d0704b6" }, + { url = "https://mirrors.aliyun.com/pypi/packages/df/29/c31c6b9e76068b3aede93a5270bcb6e285a62dbd9a8ce1f6fe8f36761d97/cachebox-5.2.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d2870529575fdf0989ad081cd585e01b2e9e30177a14696fdb6f32a7edfeae07" }, + { url = "https://mirrors.aliyun.com/pypi/packages/d7/da/f1e48ebfd1ebe336d3ab7a50f4b4f18e2854311d4cdd2c8c5da1ce13f866/cachebox-5.2.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:678f7963400981cd62973b3f8d7231b77eaae88ebee11e06d02659f565d46bc4" }, + { url = "https://mirrors.aliyun.com/pypi/packages/22/d8/05b50b50dcb7bd1cea1561f0a2a3c74cacebf129e366e268e3dfa8b12c71/cachebox-5.2.2-cp312-cp312-win32.whl", hash = "sha256:809cc6459e947951a074e4af32ad60bc6beb615e2d4a7fd76f43c157343a3be4" }, + { url = "https://mirrors.aliyun.com/pypi/packages/07/34/5ed62f3c328f4f5c6320944b23481e4650e81cc56ec7b48f3fa347b918be/cachebox-5.2.2-cp312-cp312-win_amd64.whl", hash = "sha256:9029c9ec274c594215931171ead7ecfbc61771a2db6ec5cce02b51e4fb182792" }, + { url = "https://mirrors.aliyun.com/pypi/packages/15/1b/1c4a713f2f58b8d35f933f8b4959633f90338d7283f8065d1d25074c4182/cachebox-5.2.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:935384e42a44567c33802d020c410578ee57592092123d0a68c0f5f139c4c158" }, + { url = "https://mirrors.aliyun.com/pypi/packages/9e/a7/858163cfe980dd573d4615bfc970286232123830f12744da03686d6229fc/cachebox-5.2.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1be730d2a45215e740bfb1917bb0e08240f77a6f1d824f40142395782a08b0fe" }, + { url = "https://mirrors.aliyun.com/pypi/packages/e2/8f/86663e705127b502465e8541be1d5230b12ea38076f0c208ede79b62f599/cachebox-5.2.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d425b08cdd64d9dbbf8de55c36db89850d99bd2f919278d119d4b88d3909493" }, + { url = "https://mirrors.aliyun.com/pypi/packages/46/8e/bf99e4ec106e23c0b5f4dfeee19110d4009049fb76a7135d3ee3dc5dc218/cachebox-5.2.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:aef8a9eff15352ebc4a56896fe39521109217f6b030806aaac6879e31d0b9bd6" }, + { url = "https://mirrors.aliyun.com/pypi/packages/0c/2e/f74abfda57690cf73afe8e4473d5355badf6d7d7adb9335a9556ddf49f61/cachebox-5.2.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b64feb73fed71f11ee74b645d3a91bd18fceef137805c9490d29499bc651807" }, + { url = "https://mirrors.aliyun.com/pypi/packages/c1/01/5a9481b0b46e54b2132200b60571868ca23b4faae8c5b19ba3813bf33e5f/cachebox-5.2.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:86e202ed168bf6ba23bf5139df07c44d3f5c68bc08d9de4d15be25def79b336a" }, + { url = "https://mirrors.aliyun.com/pypi/packages/03/d0/b5064b399f9c2be13f99962475b103c31bdca49c349d355f47b282979fc1/cachebox-5.2.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b273e02fc44770443ad1f4d29b3d07b1e2f275498b09fb9d874b55a99eb91d16" }, + { url = "https://mirrors.aliyun.com/pypi/packages/47/34/f973fe4d2e0327842fe4cc3c9be4b0d9c3d140cee6c1dc727b987f6d5eff/cachebox-5.2.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a00dd65a1db60058ef484e8f8519486396bfbdedc2edf43c60111ee941c08bcf" }, + { url = "https://mirrors.aliyun.com/pypi/packages/01/d4/95683aed50ba6ca8e1488cd4dd355416520a40b661a33f27f508705725b7/cachebox-5.2.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0d2bd466f4e55626e075bdb9322bf5b7afd3d31fcc963fe0a5cb6e1ffb5d9609" }, + { url = "https://mirrors.aliyun.com/pypi/packages/7a/6f/4de6f0d6a32daa6f89286abcbf24a7b68921eb7f2e460062b5c6fee984ec/cachebox-5.2.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:c69c21e4d7bf9433df63b6c06aa80a3a6423139202e2ac0fb5f82a5a3989455a" }, + { url = "https://mirrors.aliyun.com/pypi/packages/22/cc/132bc26a403bb12d0a37e88efdfd915e008af91b419680588e190cded72b/cachebox-5.2.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e3d90a427aad7592b067c1ab482ac557235b6140c6005e0f6e734ebd69edd7db" }, + { url = "https://mirrors.aliyun.com/pypi/packages/6d/35/7f64dc14b4f234c7ee4cf8bcdb20f38793a9d1c90ea4d747163479ebc1c2/cachebox-5.2.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6fa7a3ea5e51b4fc027155911c0586939133d92ce03ca2b496188ab4cfde0971" }, + { url = "https://mirrors.aliyun.com/pypi/packages/5b/85/4ab5fdb591cda9113506c6994384068428a0fca8e6dc570b64095c4f2333/cachebox-5.2.2-cp313-cp313-win32.whl", hash = "sha256:ad77a61e0f88bb1fa4625b9ff82f47314c3e729a7357e7d0748fc7232709339e" }, + { url = "https://mirrors.aliyun.com/pypi/packages/54/aa/44cfa1d0fe8ad47a392e54756f5950bc524af95258bea660558145eaaa7c/cachebox-5.2.2-cp313-cp313-win_amd64.whl", hash = "sha256:2c08080058b5a4680946201e454bd42bb0581b883b459d2f2b002735c5e33922" }, + { url = "https://mirrors.aliyun.com/pypi/packages/70/29/ad1a0d9027d41780a875fa1728d7c317d021a2bf6074e52c1969a4925fee/cachebox-5.2.2-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:d17f37752081050886463540509b0a3ec4e48be3b2fe5a827ceccbbf7a1aea76" }, + { url = "https://mirrors.aliyun.com/pypi/packages/c2/54/aacd8a79aec38baf6918f1df8c49ea0133f758ca12907dfabe2046f44c28/cachebox-5.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d9ad8a3fdc8e86bbe66d586015defc0ef6b9c38d65c2b8cf8f37e28782e86b80" }, + { url = "https://mirrors.aliyun.com/pypi/packages/ee/a2/a7d72ca01475c16f6a9a2b2d76e62810aae102bd023256e58ceefa44dd8a/cachebox-5.2.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:39db2251161a680d67b343ae0319116541976e85089dba29ba60c6048c8406b8" }, + { url = "https://mirrors.aliyun.com/pypi/packages/c9/07/e1759bcc3409de59925766df4eb29f7a3606e71efc361365749cf0b6dbf2/cachebox-5.2.2-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:effc0ae9042d499e18d6e4916e57993bb321ac8a82910aed2f31479c176aceaa" }, + { url = "https://mirrors.aliyun.com/pypi/packages/f3/c7/b07155f7fca73ac0c1cb1f76bdb7607d254c3b7d78c0cec99ab0e39148d3/cachebox-5.2.2-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7a7ab37439e898714f6dab93b687f65055d28b2b8f94e980f640192ee0b1fae5" }, + { url = "https://mirrors.aliyun.com/pypi/packages/6d/d5/f6cbec89e838022177c6f256b88e54106679e0aa63965bea85f0a0e56681/cachebox-5.2.2-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:daae7ca234c6ee5130c0c0a5145742a07293945ea22b6bd15310dbb3fff81783" }, + { url = "https://mirrors.aliyun.com/pypi/packages/a5/98/878ba4d0712bd3cfa01539f3d3483cb1b92cb949ee74b085abd507569d36/cachebox-5.2.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a175803f9bc97944a98f19648f91f101164b0c1a3f57024cd42bf5b0a95c8ec9" }, + { url = "https://mirrors.aliyun.com/pypi/packages/e6/85/ca7f3e790940eba5ebfb3fe44bac8a60deec76058c7571689348f2efee69/cachebox-5.2.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6489194eeae7c83ce3db725932af2c01bf65dfc3a3f52ba591133207cc11974e" }, + { url = "https://mirrors.aliyun.com/pypi/packages/9e/53/38a29db17c585d6c2849fd2675ac73489955b40e2659414fae302f826000/cachebox-5.2.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:405345c4fc1b97e154f321ee079dde1d5d88af60133c96ba620c685a45f52726" }, + { url = "https://mirrors.aliyun.com/pypi/packages/8b/a1/76f2b8bab61a1e02bce796bfbccb171145312241492c5217fe1e29905b25/cachebox-5.2.2-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:901e38c58cbcd5f045fa4f871ad0cc1541753234b54ad522f49e2a2f70e226cb" }, + { url = "https://mirrors.aliyun.com/pypi/packages/03/cd/ccf6872583d0aa7204ffaebc6fed434f793163b38d7fe43c151d6385458e/cachebox-5.2.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d1f2dc6cd54515f53a424632ab3277a641f7d7ffdd27a4f000383ef4bb3a148" }, + { url = "https://mirrors.aliyun.com/pypi/packages/12/66/3eab9a0ab1c6609ba3b273eb93cdb76b19ab8a45ebd6ccce77940e0d43a9/cachebox-5.2.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:3e12bfdd6aacdcf11c18483c953c63935bbef19940e03253547a25d66488a842" }, + { url = "https://mirrors.aliyun.com/pypi/packages/06/29/60f26762022a538a1479fef21a61df916610e01714df555c64a12dbd0228/cachebox-5.2.2-cp313-cp313t-win32.whl", hash = "sha256:7f841477b4e091aaf1f621eb54dc80b1e2e413ae9c6f2758db1b59d986eb6fa7" }, + { url = "https://mirrors.aliyun.com/pypi/packages/8e/7f/abb3a56fa37a8f93c243201aa44e39d7cf1811accc7df4cd560f93fc86ba/cachebox-5.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:66f287dac540212e71e4d6d47dd3a5f0a922e1ab1989923c8c667984a2a30042" }, + { url = "https://mirrors.aliyun.com/pypi/packages/ea/a1/cdb7da3228323be9078ef58ac324d0d79a07ad9ce7f8446707689cd79214/cachebox-5.2.2-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:2038809aa51466c7ebd6005a2bcb569e0f01cc7323316c0a25611b5fca37dcfb" }, + { url = "https://mirrors.aliyun.com/pypi/packages/b2/68/803c012ca8247fce1741cfdc762dbaed631355c406ee485c196f08c63f25/cachebox-5.2.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:693160cf7e23da376e44fc8265f97288bfff591ec30ce9b4f4d9a7000ebf459e" }, + { url = "https://mirrors.aliyun.com/pypi/packages/0e/d3/b19fdd7b8f5d83aa803b220d28b8ddcb04b36bed01f086543035d7884a13/cachebox-5.2.2-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ad7b9f11d3651ae0c33c94cd0536e634ed248d0845dcebfb4615300dff7ec62" }, + { url = "https://mirrors.aliyun.com/pypi/packages/ae/0e/4a44ce22efda72f8a731a8c864d6191cdd3b054c1860db3155a0f572a8c6/cachebox-5.2.2-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bc5bf83fb1f2e4e52e0bcf7196c0b95259ff09d5682608124e8290d1d816027c" }, + { url = "https://mirrors.aliyun.com/pypi/packages/c1/11/e075c6e2e7f3a62168b915a46148c465e936c3b39de6d9c11e0fec41b242/cachebox-5.2.2-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7bc9136e3469defe44e9ad481d863978097eee213f696b802b11ea80a6825b88" }, + { url = "https://mirrors.aliyun.com/pypi/packages/2e/3d/8b1b63ba9071cb3759c2ac2ebe65391775f9de507579a3946676e8dd7d70/cachebox-5.2.2-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d7cc7b1e8d63e3537f4633961086c8819a1a9717781337dedc4adebf2fe9c78f" }, + { url = "https://mirrors.aliyun.com/pypi/packages/98/11/83dfa59b01510898a3c716bc57b333360ddfccdc15a35c6894aecb71b5cc/cachebox-5.2.2-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d9eefaba6c7103e2e9b78d55a4d1a5563a9b89aa7235bbf41505318deceb917" }, + { url = "https://mirrors.aliyun.com/pypi/packages/9e/8e/ccff3957d3b098dbdf9008298877894fb852f7559a57b59b5482eb700b5c/cachebox-5.2.2-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d7e830619abb4fdbec61612c5f720eede1273fbdb4bcbe94febc94e322629cc2" }, + { url = "https://mirrors.aliyun.com/pypi/packages/14/3d/2738fede983b45b30ce15bf97a9feb11ece6e991fc09407f16dca5e37acc/cachebox-5.2.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c471ed3bb3244499ffac81ebc8739f3d9c00e08cf837ae3d1673ec0deff47bf1" }, + { url = "https://mirrors.aliyun.com/pypi/packages/8d/7b/521087b31d890fd4bbcc6c08335e5e7f079769fdadd5d8ddab47f9452d4e/cachebox-5.2.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:ecf4a20bae389f306cb8729b95a38606bb21ad1fb4a35608c4f4d73f9baa56a2" }, + { url = "https://mirrors.aliyun.com/pypi/packages/52/f5/0f8d4fc2b68229f6d7aed08a665a62eb1f34d265896c307376fb6b3d7fa6/cachebox-5.2.2-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:653567610a757add69e15e1dc6dddfe6c3cc26d93d8607406626e4bb184257ca" }, + { url = "https://mirrors.aliyun.com/pypi/packages/51/42/a2f9991b78a8cbb5659b3f54ee1ba3a731ea7ba03fdeb128b073fb9e79bf/cachebox-5.2.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e8f471ce3ebfb3cd40338113591c4f88bf1ae5b30f97ce9c92daeb66f9f44438" }, + { url = "https://mirrors.aliyun.com/pypi/packages/35/57/d4964aa418deffdf03859fc89e06ba2a6705514c4f04429dc15f48cd28db/cachebox-5.2.2-cp314-cp314-win32.whl", hash = "sha256:f94923bfe3f1ee9451c5981fa3a170d12d446fdc82860d5ef8ee32ea89ee2c0b" }, + { url = "https://mirrors.aliyun.com/pypi/packages/80/e5/26d664d73c80f45848de795dbe91790ae9575813bbc3cde5ef9c381ca73f/cachebox-5.2.2-cp314-cp314-win_amd64.whl", hash = "sha256:350c61329ae9452b6472b3eecfa7e20361781f3639a5f9ff5db2694d2547c1a9" }, + { url = "https://mirrors.aliyun.com/pypi/packages/d6/8b/a2d350f584071088cf0b5f2dfb536057dc2d4734dcbb0eb7b43454c9f03e/cachebox-5.2.2-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:2f0f4c87d74ad65d452ff988689052f7d958e73d12a3e67aa9d36bb2f2467013" }, + { url = "https://mirrors.aliyun.com/pypi/packages/90/10/495ff7b53298661ac74518778e013ec2979ad141051598d5d7b2934c28b0/cachebox-5.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:68f0e34f2cf8f40cd9f863e9e930f8eeed78ed9b61ad84438dd88b6b1b6c321f" }, + { url = "https://mirrors.aliyun.com/pypi/packages/b0/33/eea89f3fa588d367c00be3cecf002f3e5f01ad1b2dabfb3a9181c9da08bc/cachebox-5.2.2-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f1f11ebb33728a33cae112c809e5d7537fe71868350b4a2bb3489c8b15d5569" }, + { url = "https://mirrors.aliyun.com/pypi/packages/95/0d/6ce50c3345718cb7b06646607202a4bfdf8d08e893f84a24752c60915379/cachebox-5.2.2-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:157d19007843d2e4c5237d303a24da9cb35d07744e51e85a2e12fab60adb6771" }, + { url = "https://mirrors.aliyun.com/pypi/packages/ce/19/8f03d3bff454a0495a41fdaaa44504fb4b8373f08c74dca510e0474a80cd/cachebox-5.2.2-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc84c1709332060568dd584345258bc7460c28459e41cd512328a11e77fa0b74" }, + { url = "https://mirrors.aliyun.com/pypi/packages/2c/c6/6ee165e6b9c1dfbcd0d3b672d9ffa3655b056b82b9dd3d31eb12e996bdf4/cachebox-5.2.2-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fee532924171b9f836b00127afda1414a6d753bdc7e160c63aba03bce7bc54af" }, + { url = "https://mirrors.aliyun.com/pypi/packages/11/65/895d021091755b51b699b9ad179f01b13612a75edc4194eaa58621a69a9d/cachebox-5.2.2-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb32fcc0bf78c676d3377de55824057ec1068ec454745a445db6865ee1b86360" }, + { url = "https://mirrors.aliyun.com/pypi/packages/d8/63/2313b378547529f03ede0c315790ef94858f29ac2fca4e697cc82ec96b3a/cachebox-5.2.2-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:93baf8621f39353e67927d4b94ef55a36fbf8393fe7c0feb3456062dae8ad36e" }, + { url = "https://mirrors.aliyun.com/pypi/packages/61/8e/55e26bf0e6f1b1645979d3fe8a3bd316212747e23bc251a6710269e60a31/cachebox-5.2.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3ff3a7e49829ae0d92167844d77bf19cb01f19e467e1d14d5af6bd765d96bddf" }, + { url = "https://mirrors.aliyun.com/pypi/packages/17/3f/06929d96df2e2388919de1047a491f5867e523e8ad18a82efe0322ba421e/cachebox-5.2.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:9d9f2d0dc4ffc2998e6b7affa2f8318f2083cbd09a0686a5f9cc55815900d876" }, + { url = "https://mirrors.aliyun.com/pypi/packages/8c/9c/52ada501753f70a786409af32ec2263a4e0251d6269e8ed2f1abc075c48d/cachebox-5.2.2-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:e9479bf0583ec9e2ca1fdf9a5c748f1ce03ebaf8ecb4194b4758140b53e6fe44" }, + { url = "https://mirrors.aliyun.com/pypi/packages/26/8f/eb040df9ca28ab415b3d22c2356bd21169ad7bbff4dff2cbdaceac978829/cachebox-5.2.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0192492bc1a96cde1fec2adb10d7c48c3cc76e31d731305940d646dc8ed947ae" }, + { url = "https://mirrors.aliyun.com/pypi/packages/02/1a/ec14fe807a910c85b8ad310787933d59d68708bbe83992ed0bfb576ee7af/cachebox-5.2.2-cp314-cp314t-win32.whl", hash = "sha256:46d8973d1dd52f72f2aea414f5673af7bd6ae7738b9c76685904d1819d07f3d5" }, + { url = "https://mirrors.aliyun.com/pypi/packages/d7/4b/95b114f063d0ba7b5ed64646f957ce245077707331d801c1db971b83dec0/cachebox-5.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:e1fbc20c9ef09a6895f6f0894e1c272586ff3483176b2ba26118fd2c30e003a1" }, + { url = "https://mirrors.aliyun.com/pypi/packages/19/f9/2acd5e67d24d7c34280d61f929d2ed3f7ecc071d55ae4603f98e94bc06f4/cachebox-5.2.2-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5e68f06e1a748ea38afc3e3f7013a7e031c5715d9c0b832d84b32960cae6a133" }, + { url = "https://mirrors.aliyun.com/pypi/packages/60/f8/07857646ba3e66a27552c80adfc1251b4a5f948c359b286df36101079a9d/cachebox-5.2.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:b3c0bc5d6acabfc7c6b1362c38be23cb64797354f305fbd4f4d68f25bee6092b" }, + { url = "https://mirrors.aliyun.com/pypi/packages/ca/88/19b9c81487791b0b94d84f26a2b7a10009a7aaffa23730e03d415b3e7197/cachebox-5.2.2-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84dd32fadb4c5f0bb48cd38435d0e76084cf68ca9417ea31ceb764ee56eb34d4" }, + { url = "https://mirrors.aliyun.com/pypi/packages/de/bc/75347086a2c91dd9a5e6f305a58d68097ea2c6add9999f0d819e770e147f/cachebox-5.2.2-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f8d941fb5e15bea87a533ea3565412635f67dc6fd63292a4d7f4b50036248ddc" }, + { url = "https://mirrors.aliyun.com/pypi/packages/2f/9c/dee3386d5b078d8ff28c51347da5456e642f0124cdcaceb5839c3f424f8b/cachebox-5.2.2-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fb69cfb35d57f1e4faedced802f1dc61f49e584f3a7ac61fe84c246fed295765" }, + { url = "https://mirrors.aliyun.com/pypi/packages/d2/fd/331a7a2076a65d683efe46697b33cbfe44ea80dd226a1bf2f660803796d3/cachebox-5.2.2-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:13d843464830e9a81d557185df93e4730e318678c50661069005ad79dc0d2e29" }, + { url = "https://mirrors.aliyun.com/pypi/packages/f1/83/efb8fe2abec1454d131752e429a237f48b6d635f935c9001f368c22ac9c3/cachebox-5.2.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1817876f1acdda74afb1b0384700c44d15d3efe2c8e86ee7c9a910d241e6315c" }, + { url = "https://mirrors.aliyun.com/pypi/packages/fa/e9/49ddc2a65d0d11dcc30985f451a9e40a1a740e845e9ffd25b62b17095db3/cachebox-5.2.2-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fb71f8e4da1c82342f40ec0e051bd786a1e843efed632e4216301d7b99903ca3" }, + { url = "https://mirrors.aliyun.com/pypi/packages/b5/59/aa751f9a620076745133f0300e29258d89ba5e482b1f83e678b07ef96f91/cachebox-5.2.2-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:31bf9484e573e7ff5fe7aac09a69ae3e4b80192ecca7d2d8207f550fd10f2832" }, + { url = "https://mirrors.aliyun.com/pypi/packages/67/a7/ec16ff6fb2497d41ec227305525ba5ab5aae26ce8e4c529e6c70bbcb96fc/cachebox-5.2.2-pp311-pypy311_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:e2a2be7f50a4b1c5606d680153c83a7487b6bc1fa5865b16c7ff17968ca7303c" }, + { url = "https://mirrors.aliyun.com/pypi/packages/20/1b/9c08a9fd365ef9bf90130aafd056edcc4d6d5ea15739d3812629205114ec/cachebox-5.2.2-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:cc080709f667e7dceb76e63277d83c9d92a5fb52a9d9f778c6fbcc9189587f55" }, + { url = "https://mirrors.aliyun.com/pypi/packages/1a/e6/469f69c9c72d353cec4b3908b07ef90330b3e88b6f899aed54b9deb359be/cachebox-5.2.2-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:fa1efc83d59dc3bd10c6222eb1005e383c07680d18f14ccfe90d175e722d198e" }, + { url = "https://mirrors.aliyun.com/pypi/packages/ea/a9/16ac14aa5a00b9c6ef5fda91053b6409bcd545d54cb8163b50e1fc74a768/cachebox-5.2.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:b7884ce71dccfd408a6e7209deaa819687c223ed3ddaa57872dd2df6d7761638" }, +] + [[package]] name = "cappa" version = "0.31.0" @@ -692,6 +811,7 @@ dependencies = [ { name = "asyncmy" }, { name = "asyncpg" }, { name = "bcrypt" }, + { name = "cachebox" }, { name = "cappa" }, { name = "celery" }, { name = "celery-aio-pool" }, @@ -754,6 +874,7 @@ requires-dist = [ { name = "asyncmy", specifier = ">=0.2.11" }, { name = "asyncpg", specifier = ">=0.31.0" }, { name = "bcrypt", specifier = ">=5.0.0" }, + { name = "cachebox", specifier = ">=5.2.2" }, { name = "cappa", specifier = ">=0.31.0" }, { name = "celery", specifier = ">=5.6.2" }, { name = "celery-aio-pool", specifier = ">=0.1.0rc8" },