From 980a2db6d9b66e972d95030c227d94954afb89a4 Mon Sep 17 00:00:00 2001 From: PineHomePC Date: Sun, 23 Aug 2026 23:52:58 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E5=B9=B3=E5=8F=B0=E5=BA=94?= =?UTF-8?q?=E7=94=A8=E5=BC=82=E6=AD=A5=E5=9B=9B=E5=B1=82=E6=9E=B6=E6=9E=84?= =?UTF-8?q?=EF=BC=88=E6=8E=A5=E5=8F=A3/=E4=B8=9A=E5=8A=A1/=E9=A2=86?= =?UTF-8?q?=E5=9F=9F/=E5=9F=BA=E7=A1=80=E8=AE=BE=E6=96=BD=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 基础设施层 async:SQLAlchemy 异步引擎/会话、33 Repository async 化、models/security/seed 迁入 infrastructure、新增 cache.py(redis.asyncio) 与 oss.py(aioboto3) - 接口层:routers 迁 api/routers 并全 async,dependencies 迁 api/dependencies(get_db/get_current_user async) - 依赖:sqlalchemy[asyncio]/aiosqlite/asyncmy/redis/aioboto3;config 异步 URL + Redis/OSS 配置 - 删除废弃:旧同步 db/dependencies/repositories/storage - 验证:平台 19 路由 + 培训 48 路由全注册;/health /auth/login /auth/me /admin/tasks /notifications 等接口 async 可用 --- app/api/dependencies.py | 108 ++ app/{ => api}/routers/__init__.py | 0 app/{ => api}/routers/agents.py | 16 +- app/{ => api}/routers/auth.py | 80 +- app/{ => api}/routers/bootstrap.py | 2 +- app/{ => api}/routers/rbac_admin.py | 58 +- app/{ => api}/routers/rbac_developer.py | 16 +- app/{ => api}/routers/rbac_ecosystem.py | 64 +- app/{ => api}/routers/rbac_government.py | 32 +- app/{ => api}/routers/rbac_investor.py | 48 +- app/{ => api}/routers/rbac_opc.py | 74 +- app/{ => api}/routers/rbac_operator.py | 44 +- app/{ => api}/routers/rbac_org.py | 26 +- app/{ => api}/routers/rbac_portals.py | 38 +- app/{ => api}/routers/templates.py | 2 +- app/config.py | 15 +- app/db.py | 4 +- app/infrastructure/__init__.py | 2 + app/infrastructure/cache.py | 60 + app/infrastructure/db.py | 65 + app/{ => infrastructure}/models.py | 0 app/infrastructure/oss.py | 63 + app/infrastructure/repositories.py | 1804 ++++++++++++++++++++++ app/{ => infrastructure}/security.py | 0 app/{ => infrastructure}/seed.py | 82 +- app/main.py | 41 +- app/rbac.py | 10 +- pyproject.toml | 6 +- uv.lock | 890 ++++++++++- 29 files changed, 3327 insertions(+), 323 deletions(-) create mode 100644 app/api/dependencies.py rename app/{ => api}/routers/__init__.py (100%) rename app/{ => api}/routers/agents.py (83%) rename app/{ => api}/routers/auth.py (81%) rename app/{ => api}/routers/bootstrap.py (95%) rename app/{ => api}/routers/rbac_admin.py (74%) rename app/{ => api}/routers/rbac_developer.py (73%) rename app/{ => api}/routers/rbac_ecosystem.py (75%) rename app/{ => api}/routers/rbac_government.py (85%) rename app/{ => api}/routers/rbac_investor.py (78%) rename app/{ => api}/routers/rbac_opc.py (75%) rename app/{ => api}/routers/rbac_operator.py (80%) rename app/{ => api}/routers/rbac_org.py (80%) rename app/{ => api}/routers/rbac_portals.py (83%) rename app/{ => api}/routers/templates.py (98%) create mode 100644 app/infrastructure/__init__.py create mode 100644 app/infrastructure/cache.py create mode 100644 app/infrastructure/db.py rename app/{ => infrastructure}/models.py (100%) create mode 100644 app/infrastructure/oss.py create mode 100644 app/infrastructure/repositories.py rename app/{ => infrastructure}/security.py (100%) rename app/{ => infrastructure}/seed.py (95%) diff --git a/app/api/dependencies.py b/app/api/dependencies.py new file mode 100644 index 0000000..e7959ab --- /dev/null +++ b/app/api/dependencies.py @@ -0,0 +1,108 @@ +# -*- coding: utf-8 -*- +"""接口层依赖:数据库句柄与当前登录用户(JWT + RBAC,全异步)。 + +``Database`` 实例由 ``main.py`` 在启动时创建并挂在 ``app.state.db`` 上, +路由通过 ``Depends(get_db)`` 取用;测试时可替换为临时目录实例。 +""" +from __future__ import annotations + +from fastapi import Depends, HTTPException, Request + +from .. import config +from ..infrastructure.repositories import Database +from ..jwt import decode_access_token + +BEARER_PREFIX = "Bearer " + + +def extract_bearer_token(request: Request) -> str: + """从 Authorization 头提取 Bearer token,没有则返回空串。""" + auth_header = request.headers.get("Authorization", "") + if auth_header.startswith(BEARER_PREFIX): + return auth_header[len(BEARER_PREFIX):].strip() + return "" + + +def get_db(request: Request) -> Database: + return request.app.state.db + + +async def _resolve_identity(user: dict, identity_id: str | None, db: Database) -> dict: + """按令牌携带的 identity_id 装配当前身份;缺失则回退到 users.role 单角色。 + + 身份即权限来源:从 user_identities 取 role/sub_role/org/region, + 并据此计算 permissions 与数据范围(全异步,角色变更即时生效)。 + """ + if identity_id: + ident = await db.identities.get_for_user(identity_id, user["id"]) + if ident and ident.get("status") == "active": + user["identity_id"] = ident["id"] + user["port"] = ident["port"] + user["role"] = ident["role"] + user["sub_role"] = ident.get("sub_role") + user["org_id"] = ident.get("org_id") + user["region_id"] = ident.get("region_id") + user["identity_name"] = ident.get("name", "") + + user["permissions"] = await db.roles.permissions_for( + user.get("role", "opc_member"), user.get("sub_role"), + ) + user["scope_region_ids"] = await db.regions.visible_region_ids(user.get("region_id")) + user["scope_level"] = await db.regions.level(user.get("region_id")) + return user + + +async def get_current_user( + request: Request, + db: Database = Depends(get_db), +) -> dict: + """校验调用方 JWT,返回当前用户记录(含 role/权限/数据范围)。""" + token = extract_bearer_token(request) + if not token: + raise HTTPException(status_code=401, detail="No token provided") + + payload = decode_access_token(token) + if payload is None: + raise HTTPException(status_code=401, detail="Invalid or expired token") + + user_id = payload.get("sub") + if not await db.tokens.session_valid( + payload.get("jti", ""), + user_id, + payload.get("ver", 0), + ): + raise HTTPException(status_code=401, detail="Invalid or expired token") + + user = await db.users.get_by_id(user_id) + if user is None or user.get("status") != "active": + raise HTTPException(status_code=401, detail="Invalid or expired token") + + return await _resolve_identity(user, payload.get("identity_id"), db) + + +async def optional_current_user( + request: Request, + db: Database = Depends(get_db), +) -> dict | None: + """可选登录:有有效 JWT 则返回用户,否则返回 None(不报错)。""" + token = extract_bearer_token(request) + if not token: + return None + payload = decode_access_token(token) + if payload is None: + return None + if not await db.tokens.session_valid( + payload.get("jti", ""), payload.get("sub", ""), payload.get("ver", 0), + ): + return None + user = await db.users.get_by_id(payload.get("sub")) + if user is None or user.get("status") != "active": + return None + return await _resolve_identity(user, payload.get("identity_id"), db) + + +def require_port(user: dict = Depends(get_current_user)) -> dict: + """按端口隔离的资源(如智能体):必须已解析出端口身份,否则 403。""" + if not user.get("port"): + raise HTTPException(status_code=403, detail="请先选择身份/端口") + return user diff --git a/app/routers/__init__.py b/app/api/routers/__init__.py similarity index 100% rename from app/routers/__init__.py rename to app/api/routers/__init__.py diff --git a/app/routers/agents.py b/app/api/routers/agents.py similarity index 83% rename from app/routers/agents.py rename to app/api/routers/agents.py index 22773fe..bcb354f 100644 --- a/app/routers/agents.py +++ b/app/api/routers/agents.py @@ -9,8 +9,8 @@ from __future__ import annotations from fastapi import APIRouter, Depends, HTTPException from ..dependencies import get_db, require_port -from ..models import AgentCreate, AgentInfo, AgentUpdate -from ..repositories import Database +from ...infrastructure.models import AgentCreate, AgentInfo, AgentUpdate +from ...infrastructure.repositories import Database router = APIRouter(prefix="/agents", tags=["agents"]) @@ -28,7 +28,7 @@ async def list_agents( user: dict = Depends(require_port), db: Database = Depends(get_db), ): - return [_agent_info(r) for r in db.agents.get_by_user(user["id"], port=user["port"])] + return [_agent_info(r) for r in await db.agents.get_by_user(user["id"], port=user["port"])] @router.get("/{agent_id}", response_model=AgentInfo, summary="智能体详情") @@ -37,7 +37,7 @@ async def get_agent( user: dict = Depends(require_port), db: Database = Depends(get_db), ): - record = db.agents.get(agent_id, user["id"], port=user["port"]) + record = await db.agents.get(agent_id, user["id"], port=user["port"]) if record is None: raise HTTPException(status_code=404, detail="Agent not found") return _agent_info(record) @@ -56,7 +56,7 @@ async def create_agent( ): if not req.name.strip(): raise HTTPException(status_code=400, detail="Agent name is required") - record = db.agents.create( + record = await db.agents.create( user["id"], req.name, description=req.description, @@ -77,7 +77,7 @@ async def update_agent( payload = req.model_dump(exclude_none=True) if not payload: raise HTTPException(status_code=400, detail="Nothing to update") - record = db.agents.update(agent_id, user["id"], payload, port=user["port"]) + record = await db.agents.update(agent_id, user["id"], payload, port=user["port"]) if record is None: raise HTTPException(status_code=404, detail="Agent not found") return _agent_info(record) @@ -89,7 +89,7 @@ async def delete_agent( user: dict = Depends(require_port), db: Database = Depends(get_db), ): - record = db.agents.get(agent_id, user["id"], port=user["port"]) + record = await db.agents.get(agent_id, user["id"], port=user["port"]) if record is None: raise HTTPException(status_code=404, detail="Agent not found") if not record.get("deletable", True): @@ -97,6 +97,6 @@ async def delete_agent( status_code=400, detail="Cannot delete this agent", ) - if not db.agents.delete(agent_id, user["id"], port=user["port"]): + if not await db.agents.delete(agent_id, user["id"], port=user["port"]): raise HTTPException(status_code=404, detail="Agent not found") return None diff --git a/app/routers/auth.py b/app/api/routers/auth.py similarity index 81% rename from app/routers/auth.py rename to app/api/routers/auth.py index ebc692c..7906d55 100644 --- a/app/routers/auth.py +++ b/app/api/routers/auth.py @@ -11,9 +11,9 @@ import re from fastapi import APIRouter, Depends, HTTPException, Request -from .. import config +from ... import config from ..dependencies import get_current_user, get_db, extract_bearer_token -from ..models import ( +from ...infrastructure.models import ( AuthStatusResponse, IdentityInfo, LoginRequest, @@ -25,7 +25,7 @@ from ..models import ( UpdateProfileRequest, VerifyResponse, ) -from ..repositories import Database +from ...infrastructure.repositories import Database router = APIRouter(prefix="/auth", tags=["auth"]) @@ -59,7 +59,7 @@ def _identity_summaries(identities: list[dict]) -> list[IdentityInfo]: ] -def _issue_token( +async def _issue_token( db: Database, user: dict, expires_in: int | None, @@ -76,13 +76,13 @@ def _issue_token( region_id = (identity or user).get("region_id") identity_id = identity.get("id") if identity else None - perms = db.roles.permissions_for(role, sub_role) - scope_ids = db.regions.visible_region_ids(region_id) - scope_level = db.regions.level(region_id) + perms = await db.roles.permissions_for(role, sub_role) + scope_ids = await db.regions.visible_region_ids(region_id) + scope_level = await db.regions.level(region_id) token_user = dict(user) token_user.update(role=role, sub_role=sub_role, org_id=org_id, region_id=region_id) - return db.tokens.create( + return await db.tokens.create( token_user, permissions=perms, scope_region_ids=scope_ids, @@ -92,7 +92,7 @@ def _issue_token( ) -def _profile_for( +async def _profile_for( user: dict, identity: dict | None, db: Database, @@ -108,8 +108,8 @@ def _profile_for( profile["sub_role"] = sub_role profile["org_id"] = org_id profile["region_id"] = region_id - profile["permissions"] = db.roles.permissions_for(role, sub_role) - profile["scope_region_ids"] = db.regions.visible_region_ids(region_id) + profile["permissions"] = await db.roles.permissions_for(role, sub_role) + profile["scope_region_ids"] = await db.regions.visible_region_ids(region_id) if identity: profile["identity_id"] = identity["id"] profile["port"] = identity["port"] @@ -125,18 +125,18 @@ async def login(req: LoginRequest, db: Database = Depends(get_db)): - 多个身份:签发中性账号令牌,前端展示身份选择,经 ``/auth/select-identity`` 切换到指定身份后进入对应端口。 """ - user = db.users.get_by_username(req.username) - if user is None or not db.users.verify_password(user, req.password): + user = await db.users.get_by_username(req.username) + if user is None or not await db.users.verify_password(user, req.password): raise HTTPException(status_code=401, detail="Invalid username or password") if user.get("status") != "active": raise HTTPException(status_code=403, detail="Account is disabled") - identities = db.identities.list_for_user(user["id"], active_only=True) + identities = await db.identities.list_for_user(user["id"], active_only=True) identity = identities[0] if len(identities) == 1 else None - token_record = _issue_token(db, user, req.expires_in, identity=identity) - profile = _profile_for(user, identity, db) + token_record = await _issue_token(db, user, req.expires_in, identity=identity) + profile = await _profile_for(user, identity, db) - db.audit.add( + await db.audit.add( action="login", resource="auth", resource_id=user["id"], detail=f"login {user['username']} ({len(identities)} identities)", user_id=user["id"], @@ -155,13 +155,13 @@ async def select_identity( db: Database = Depends(get_db), ): """把当前令牌切换到指定端口身份(该身份须属于当前账号且为启用状态)。""" - ident = db.identities.get_for_user(req.identity_id, user["id"]) + ident = await db.identities.get_for_user(req.identity_id, user["id"]) if ident is None or ident.get("status") != "active": raise HTTPException(status_code=404, detail="Identity not found or disabled") - token_record = _issue_token(db, user, None, identity=ident) - profile = _profile_for(user, ident, db) - identities = db.identities.list_for_user(user["id"], active_only=True) + token_record = await _issue_token(db, user, None, identity=ident) + profile = await _profile_for(user, ident, db) + identities = await db.identities.list_for_user(user["id"], active_only=True) return LoginResponse( token=token_record["token"], identities=_identity_summaries(identities), @@ -179,7 +179,7 @@ async def register(req: RegisterRequest, db: Database = Depends(get_db)): ) if not config.AUTH_ENABLED: raise HTTPException(status_code=403, detail="Authentication is not enabled") - if db.users.has_users(): + if await db.users.has_users(): raise HTTPException(status_code=403, detail="User already registered") if not req.username.strip() or not req.password.strip(): raise HTTPException( @@ -187,13 +187,13 @@ async def register(req: RegisterRequest, db: Database = Depends(get_db)): detail="Username and password are required", ) - user = db.users.create(req.username, req.password) - identity = db.identities.create( + user = await db.users.create(req.username, req.password) + identity = await db.identities.create( user["id"], port="opc", role="opc_member", sub_role="independent", name="独立OPC", ) - token_record = _issue_token(db, user, req.expires_in, identity=identity) - profile = _profile_for(user, identity, db) + token_record = await _issue_token(db, user, req.expires_in, identity=identity) + profile = await _profile_for(user, identity, db) return LoginResponse( token=token_record["token"], identities=_identity_summaries([identity]), @@ -206,7 +206,7 @@ async def auth_status(db: Database = Depends(get_db)): """前端登录页据此判断是否展示登录表单。""" return AuthStatusResponse( enabled=config.AUTH_ENABLED, - has_users=db.users.has_users(), + has_users=await db.users.has_users(), ) @@ -225,7 +225,7 @@ async def me( db: Database = Depends(get_db), ): """返回当前登录用户的完整资料(含角色/组织/区域/权限/数据范围)。""" - return db.users.to_profile(user) + return await db.users.to_profile(user) @router.post("/update-profile", response_model=ProfileResponse, summary="更新资料/凭据") @@ -256,28 +256,28 @@ async def update_profile( if not profile_updates and not changing_credentials: raise HTTPException(status_code=400, detail="Nothing to update") - if changing_credentials and not db.users.verify_password(user, req.current_password): + if changing_credentials and not await db.users.verify_password(user, req.current_password): raise HTTPException(status_code=401, detail="Current password is incorrect") if profile_updates: - db.users.update_profile(user_id, profile_updates) + await db.users.update_profile(user_id, profile_updates) issued_token = "" if changing_credentials: - db.users.update_credentials(user_id, new_username, new_password) - db.tokens.revoke_all(user_id) - fresh_user = db.users.get_by_id(user_id) - token_record = _issue_token(db, fresh_user, req.expires_in) + await db.users.update_credentials(user_id, new_username, new_password) + await db.tokens.revoke_all(user_id) + fresh_user = await db.users.get_by_id(user_id) + token_record = await _issue_token(db, fresh_user, req.expires_in) issued_token = token_record["token"] - fresh_user = db.users.get_by_id(user_id) - fresh_user["permissions"] = db.roles.permissions_for( + fresh_user = await db.users.get_by_id(user_id) + fresh_user["permissions"] = await db.roles.permissions_for( fresh_user.get("role", "opc_member"), fresh_user.get("sub_role"), ) - fresh_user["scope_region_ids"] = db.regions.visible_region_ids(fresh_user.get("region_id")) + fresh_user["scope_region_ids"] = await db.regions.visible_region_ids(fresh_user.get("region_id")) return ProfileResponse( token=issued_token, - **db.users.to_profile(fresh_user), + **await db.users.to_profile(fresh_user), ) @@ -293,7 +293,7 @@ async def revoke_single_token( token_to_revoke = req.token or caller_token is_current = token_to_revoke == caller_token - if not db.tokens.revoke(token_to_revoke): + if not await db.tokens.revoke(token_to_revoke): raise HTTPException(status_code=500, detail="Failed to revoke token") message = ( @@ -314,7 +314,7 @@ async def revoke_all_sessions( db: Database = Depends(get_db), ): """吊销所有令牌,所有会话需重新登录。""" - db.tokens.revoke_all() + await db.tokens.revoke_all() return { "message": "All tokens have been revoked. Please login again.", "revoked": True, diff --git a/app/routers/bootstrap.py b/app/api/routers/bootstrap.py similarity index 95% rename from app/routers/bootstrap.py rename to app/api/routers/bootstrap.py index 818fd78..bc76689 100644 --- a/app/routers/bootstrap.py +++ b/app/api/routers/bootstrap.py @@ -9,7 +9,7 @@ from __future__ import annotations from fastapi import APIRouter -from ..repositories import AGENT_SEED +from ...infrastructure.repositories import AGENT_SEED router = APIRouter(tags=["bootstrap"]) diff --git a/app/routers/rbac_admin.py b/app/api/routers/rbac_admin.py similarity index 74% rename from app/routers/rbac_admin.py rename to app/api/routers/rbac_admin.py index 99e1661..f65d247 100644 --- a/app/routers/rbac_admin.py +++ b/app/api/routers/rbac_admin.py @@ -6,9 +6,9 @@ from fastapi import APIRouter, Depends, HTTPException, Request from pydantic import BaseModel from ..dependencies import get_db -from ..models import SetUserRoleRequest, SetUserStatusRequest -from ..rbac import require_permission, require_roles, write_audit -from ..repositories import Database +from ...infrastructure.models import SetUserRoleRequest, SetUserStatusRequest +from ...rbac import require_permission, require_roles, write_audit +from ...infrastructure.repositories import Database router = APIRouter(prefix="/admin", tags=["admin"]) @@ -27,15 +27,15 @@ class RolePermissionRequest(BaseModel): permissions: list[str] = [] -def _validate_role_assignment(db: Database, actor: dict, role: str, sub_role: str | None) -> None: +async def _validate_role_assignment(db: Database, actor: dict, role: str, sub_role: str | None) -> None: """白名单 + 提权防护:拒绝非法角色组合,及无权者创建/提升特权角色。""" key = role if not sub_role else f"{role}|{sub_role}" - if not db.roles.role_exists(role): + if not await db.roles.role_exists(role): raise HTTPException(status_code=400, detail=f"无效角色: {role}") - if sub_role and not db.roles.role_exists(f"{role}|{sub_role}"): + if sub_role and not await db.roles.role_exists(f"{role}|{sub_role}"): raise HTTPException(status_code=400, detail=f"无效角色组合: {key}") # 目标角色若授予「配置角色权限」,仅持该权限者(超管)可创建/分配 - if "action:role.grant_perm" in db.roles.permissions_for(role, sub_role) and \ + if "action:role.grant_perm" in await db.roles.permissions_for(role, sub_role) and \ "action:role.grant_perm" not in actor.get("permissions", []): raise HTTPException( status_code=403, @@ -52,20 +52,20 @@ async def create_user( ): if not req.username.strip() or not req.password.strip(): raise HTTPException(status_code=400, detail="账号与密码必填") - if db.users.get_by_username(req.username) is not None: + if await db.users.get_by_username(req.username) is not None: raise HTTPException(status_code=400, detail="账号已存在") - _validate_role_assignment(db, actor, req.role, req.sub_role) - user = db.users.create( + await _validate_role_assignment(db, actor, req.role, req.sub_role) + user = await db.users.create( req.username, req.password, nickname=req.nickname, role=req.role, sub_role=req.sub_role, org_id=req.org_id, region_id=req.region_id, ) # 自动创建该账号的默认端口身份 - db.identities.create(user["id"], port=_port_for_role(req.role), role=req.role, + await db.identities.create(user["id"], port=_port_for_role(req.role), role=req.role, sub_role=req.sub_role, org_id=req.org_id, region_id=req.region_id, name=req.nickname or req.username) - write_audit(db, action="user.create", resource="user", resource_id=user["id"], + await write_audit(db, action="user.create", resource="user", resource_id=user["id"], detail=f"role={req.role}", user=actor, request=request) - return db.users.to_profile(user) + return await db.users.to_profile(user) def _port_for_role(role: str) -> str: @@ -83,7 +83,7 @@ async def list_users( _role: dict = Depends(require_roles("operator")), _perm: dict = Depends(require_permission("menu:admin_user_mgmt")), ): - return db.users.list() + return await db.users.list() @router.post("/users/{user_id}/role", summary="分配角色") @@ -94,18 +94,18 @@ async def set_user_role( db: Database = Depends(get_db), actor: dict = Depends(require_permission("action:user.assign_role")), ): - if db.users.get_by_id(user_id) is None: + if await db.users.get_by_id(user_id) is None: raise HTTPException(status_code=404, detail="User not found") - _validate_role_assignment(db, actor, req.role, req.sub_role) - updated = db.users.set_role( + await _validate_role_assignment(db, actor, req.role, req.sub_role) + updated = await db.users.set_role( user_id, req.role, req.sub_role, req.org_id, req.region_id, ) - write_audit( + await write_audit( db, action="role.assign", resource="user", resource_id=user_id, detail=f"{actor['username']} -> role={req.role} sub={req.sub_role}", user=actor, request=request, ) - return {"ok": True, "user": db.users.to_profile(updated)} + return {"ok": True, "user": await db.users.to_profile(updated)} @router.post("/users/{user_id}/status", summary="禁用/启用用户") @@ -116,15 +116,15 @@ async def set_user_status( db: Database = Depends(get_db), actor: dict = Depends(require_permission("action:user.disable")), ): - if db.users.get_by_id(user_id) is None: + if await db.users.get_by_id(user_id) is None: raise HTTPException(status_code=404, detail="User not found") - updated = db.users.set_status(user_id, req.status) - write_audit( + updated = await db.users.set_status(user_id, req.status) + await write_audit( db, action="user.disable", resource="user", resource_id=user_id, detail=f"{actor['username']} -> status={req.status}", user=actor, request=request, ) - return {"ok": True, "user": db.users.to_profile(updated)} + return {"ok": True, "user": await db.users.to_profile(updated)} @router.get("/roles", summary="角色列表") @@ -132,7 +132,7 @@ async def list_roles( db: Database = Depends(get_db), _user: dict = Depends(require_permission("menu:admin_role_mgmt")), ): - return db.roles.list_roles() + return await db.roles.list_roles() @router.get("/permissions", summary="权限列表") @@ -140,7 +140,7 @@ async def list_permissions( db: Database = Depends(get_db), _user: dict = Depends(require_permission("menu:admin_role_mgmt")), ): - return db.roles.list_permissions() + return await db.roles.list_permissions() @router.post("/roles/{role_id}/permissions", summary="配置角色权限") @@ -151,10 +151,10 @@ async def set_role_permissions( db: Database = Depends(get_db), actor: dict = Depends(require_permission("action:role.grant_perm")), ): - if not db.roles.role_exists(role_id): + if not await db.roles.role_exists(role_id): raise HTTPException(status_code=404, detail="Role not found") - perms = db.roles.set_role_permissions(role_id, req.permissions) - write_audit(db, action="role.grant_perm", resource="role", resource_id=role_id, + perms = await db.roles.set_role_permissions(role_id, req.permissions) + await write_audit(db, action="role.grant_perm", resource="role", resource_id=role_id, detail=f"permissions={len(req.permissions)}", user=actor, request=request) return {"ok": True, "role_id": role_id, "permissions": perms} @@ -166,4 +166,4 @@ async def list_audit_logs( db: Database = Depends(get_db), _user: dict = Depends(require_permission("action:audit.view")), ): - return db.audit.list(limit=min(limit, 500), offset=offset) + return await db.audit.list(limit=min(limit, 500), offset=offset) diff --git a/app/routers/rbac_developer.py b/app/api/routers/rbac_developer.py similarity index 73% rename from app/routers/rbac_developer.py rename to app/api/routers/rbac_developer.py index 5595d0c..a85457f 100644 --- a/app/routers/rbac_developer.py +++ b/app/api/routers/rbac_developer.py @@ -5,37 +5,37 @@ from __future__ import annotations from fastapi import APIRouter, Depends from ..dependencies import get_db -from ..rbac import require_roles -from ..repositories import Database +from ...rbac import require_roles +from ...infrastructure.repositories import Database router = APIRouter(prefix="/developer", tags=["developer"]) -def _page(port: str, page: str): +async def _page(port: str, page: str): async def handler(db: Database = Depends(get_db)): - return db.portal_pages.get(port, page) or {"items": []} + return await db.portal_pages.get(port, page) or {"items": []} return handler @router.get("/dashboard", summary="开发者工作台") async def dev_dashboard(db: Database = Depends(get_db), _u: dict = Depends(require_roles("developer"))): - return db.portal_pages.get("developer", "dashboard") or {"stats": []} + return await db.portal_pages.get("developer", "dashboard") or {"stats": []} @router.get("/apps", summary="我的应用") async def dev_apps(db: Database = Depends(get_db), _u: dict = Depends(require_roles("developer"))): - return db.portal_pages.get("developer", "apps") or {"items": []} + return await db.portal_pages.get("developer", "apps") or {"items": []} @router.get("/plugins", summary="我的插件") async def dev_plugins(db: Database = Depends(get_db), _u: dict = Depends(require_roles("developer"))): - return db.portal_pages.get("developer", "plugins") or {"items": []} + return await db.portal_pages.get("developer", "plugins") or {"items": []} @router.get("/skills", summary="技能市场") async def dev_skills(db: Database = Depends(get_db), _u: dict = Depends(require_roles("developer"))): - return db.portal_pages.get("developer", "skills") or {"items": []} + return await db.portal_pages.get("developer", "skills") or {"items": []} @router.get("/api-keys", summary="API 凭证") diff --git a/app/routers/rbac_ecosystem.py b/app/api/routers/rbac_ecosystem.py similarity index 75% rename from app/routers/rbac_ecosystem.py rename to app/api/routers/rbac_ecosystem.py index a98618d..8f74bce 100644 --- a/app/routers/rbac_ecosystem.py +++ b/app/api/routers/rbac_ecosystem.py @@ -7,8 +7,8 @@ from fastapi import APIRouter, Depends, HTTPException, Request from pydantic import BaseModel from ..dependencies import get_db -from ..rbac import require_permission, require_roles, write_audit -from ..repositories import Database +from ...rbac import require_permission, require_roles, write_audit +from ...infrastructure.repositories import Database router = APIRouter(tags=["ecosystem"]) @@ -21,7 +21,7 @@ async def list_notifications( db: Database = Depends(get_db), user: dict = Depends(require_roles(*_ANY)), ): - return {"items": db.notifications.list_for(user["id"]), "unread": db.notifications.unread(user["id"])} + return {"items": await db.notifications.list_for(user["id"]), "unread": await db.notifications.unread(user["id"])} @router.post("/notifications/read-all", summary="全部已读") @@ -29,7 +29,7 @@ async def read_all_notifications( db: Database = Depends(get_db), user: dict = Depends(require_roles(*_ANY)), ): - return {"marked": db.notifications.mark_read(user["id"])} + return {"marked": await db.notifications.mark_read(user["id"])} # ── 信用/评价 ────────────────────────────────────────────────────────────── @@ -38,10 +38,10 @@ async def my_credit( db: Database = Depends(get_db), user: dict = Depends(require_roles(*_ANY)), ): - profile = db.opc_profiles.get(user["id"]) + profile = await db.opc_profiles.get(user["id"]) return {"credit_score": (profile or {}).get("credit_score", 80), - "avg_rating": db.ratings.avg_for(user["id"]), - "rating_count": len(db.ratings.list_for(user["id"])) if hasattr(db.ratings, "list_for") else 0} + "avg_rating": await db.ratings.avg_for(user["id"]), + "rating_count": len(await db.ratings.list_for(user["id"])) if hasattr(db.ratings, "list_for") else 0} class RatingRequest(BaseModel): @@ -60,8 +60,8 @@ async def rate_task( ): if not req.to_id: raise HTTPException(status_code=400, detail="缺少被评对象") - rating = db.ratings.create(task_id, actor["id"], req.to_id, req.score, req.comment) - write_audit(db, action="task.rate", resource="task", resource_id=task_id, + rating = await db.ratings.create(task_id, actor["id"], req.to_id, req.score, req.comment) + await write_audit(db, action="task.rate", resource="task", resource_id=task_id, detail=f"{req.score}星", user=actor, request=request) return rating @@ -73,7 +73,7 @@ async def get_contract( db: Database = Depends(get_db), _u: dict = Depends(require_roles("enterprise", "opc_member")), ): - contract = db.contracts.get_for_task(task_id) + contract = await db.contracts.get_for_task(task_id) if contract is None: raise HTTPException(status_code=404, detail="Contract not found") return contract @@ -91,14 +91,14 @@ async def sign_contract( db: Database = Depends(get_db), actor: dict = Depends(require_roles("enterprise", "opc_member")), ): - task = db.tasks.get(task_id) + task = await db.tasks.get(task_id) if task is None: raise HTTPException(status_code=404, detail="Task not found") - if db.contracts.get_for_task(task_id) is not None: - return db.contracts.get_for_task(task_id) + if await db.contracts.get_for_task(task_id) is not None: + return await db.contracts.get_for_task(task_id) opc_id = req.opc_id or (actor["id"] if actor.get("role") == "opc_member" else "") - contract = db.contracts.create(task_id, task["title"], actor["id"], opc_id) - write_audit(db, action="contract.sign", resource="contract", resource_id=contract["id"], + contract = await db.contracts.create(task_id, task["title"], actor["id"], opc_id) + await write_audit(db, action="contract.sign", resource="contract", resource_id=contract["id"], user=actor, request=request) return contract @@ -111,17 +111,17 @@ async def release_escrow( db: Database = Depends(get_db), actor: dict = Depends(require_roles("enterprise")), ): - task = db.tasks.get(task_id) + task = await db.tasks.get(task_id) if task is None or task["status"] != "completed": raise HTTPException(status_code=400, detail="任务未完成,不能结算") - escrow = db.escrows.list() + escrow = await db.escrows.list() esc = next((e for e in escrow if e["task_id"] == task_id), None) if esc is None: amount = max(task.get("budget_min", 0), task.get("budget_max", 0)) commission = int(amount * 0.05) - esc = db.escrows.create(task_id, task["title"], amount, commission) - released = db.escrows.set_status(esc["id"], "released") - write_audit(db, action="escrow.release", resource="escrow", resource_id=esc["id"], + esc = await db.escrows.create(task_id, task["title"], amount, commission) + released = await db.escrows.set_status(esc["id"], "released") + await write_audit(db, action="escrow.release", resource="escrow", resource_id=esc["id"], detail=f"amount={esc['amount']}", user=actor, request=request) return released @@ -132,7 +132,7 @@ async def operator_settlements( db: Database = Depends(get_db), _u: dict = Depends(require_permission("action:settlement.manage")), ): - return {"items": db.escrows.list(status=status)} + return {"items": await db.escrows.list(status=status)} # ── 争议 ────────────────────────────────────────────────────────────────── @@ -148,12 +148,12 @@ async def create_dispute( db: Database = Depends(get_db), actor: dict = Depends(require_roles("enterprise", "opc_member")), ): - task = db.tasks.get(task_id) + task = await db.tasks.get(task_id) if task is None: raise HTTPException(status_code=404, detail="Task not found") - dispute = db.disputes.create(task_id, task["title"], actor["id"], req.reason) - db.tasks.set_status(task_id, "disputed") - write_audit(db, action="dispute.open", resource="dispute", resource_id=dispute["id"], + dispute = await db.disputes.create(task_id, task["title"], actor["id"], req.reason) + await db.tasks.set_status(task_id, "disputed") + await write_audit(db, action="dispute.open", resource="dispute", resource_id=dispute["id"], user=actor, request=request) return dispute @@ -165,10 +165,10 @@ async def resolve_dispute( db: Database = Depends(get_db), actor: dict = Depends(require_roles("operator")), ): - dispute = db.disputes.set_status(dispute_id, "resolved", resolution="平台调解结案") + dispute = await db.disputes.set_status(dispute_id, "resolved", resolution="平台调解结案") if dispute is None: raise HTTPException(status_code=404, detail="Dispute not found") - write_audit(db, action="dispute.resolve", resource="dispute", resource_id=dispute_id, + await write_audit(db, action="dispute.resolve", resource="dispute", resource_id=dispute_id, user=actor, request=request) return dispute @@ -179,9 +179,9 @@ async def investor_matches( db: Database = Depends(get_db), user: dict = Depends(require_roles("investor")), ): - pref = db.investor_prefs.get(user["id"]) or {} + pref = await db.investor_prefs.get(user["id"]) or {} industries = set(pref.get("industries") or []) - data = db.portal_pages.get("investor", "projects") or {"items": []} + data = await db.portal_pages.get("investor", "projects") or {"items": []} items = data.get("items", []) scored = [] for it in items: @@ -207,11 +207,11 @@ async def join_roadshow( db: Database = Depends(get_db), actor: dict = Depends(require_roles("investor", "government", "operator", "carrier", "enterprise")), ): - rs = db.roadshows.get(roadshow_id) + rs = await db.roadshows.get(roadshow_id) if rs is None: raise HTTPException(status_code=404, detail="Roadshow not found") # 标记出席(若已报名则更新为 attended;否则记录出席) - db.roadshow_regs.create(roadshow_id, actor["id"], role="attendee") - write_audit(db, action="roadshow.join", resource="roadshow", resource_id=roadshow_id, + await db.roadshow_regs.create(roadshow_id, actor["id"], role="attendee") + await write_audit(db, action="roadshow.join", resource="roadshow", resource_id=roadshow_id, user=actor, request=request) return {"joined": True, "live_url": rs.get("live_url") or "http://live.example/roadshow"} \ No newline at end of file diff --git a/app/routers/rbac_government.py b/app/api/routers/rbac_government.py similarity index 85% rename from app/routers/rbac_government.py rename to app/api/routers/rbac_government.py index 1a2ed0b..f8d201b 100644 --- a/app/routers/rbac_government.py +++ b/app/api/routers/rbac_government.py @@ -5,8 +5,8 @@ from __future__ import annotations from fastapi import APIRouter, Depends, HTTPException, Request from ..dependencies import get_db -from ..rbac import require_permission, require_scope, write_audit -from ..repositories import Database +from ...rbac import require_permission, require_scope, write_audit +from ...infrastructure.repositories import Database router = APIRouter(prefix="/government", tags=["government"]) @@ -17,7 +17,7 @@ async def list_regions( user: dict = Depends(require_scope()), ): """返回当前政务账号数据范围内的区域(省级=全部,区县=仅本区县)。""" - all_regions = db.regions.all() + all_regions = await db.regions.all() visible = {rid for rid in user.get("scope_region_ids", [])} return [r for r in all_regions if r["id"] in visible] @@ -29,9 +29,9 @@ async def list_enterprises( user: dict = Depends(require_permission("menu:gov_data")), ): """返回数据范围内的甲方企业;每次访问写一条 data.view 审计。""" - orgs = db.orgs.list_by_region(user.get("scope_region_ids", [])) + orgs = await db.orgs.list_by_region(user.get("scope_region_ids", [])) enterprises = [o for o in orgs if o["type"] == "enterprise"] - write_audit( + await write_audit( db, action="data.view", resource="enterprise", detail=f"scope={user.get('scope_region_ids', [])}", user=user, request=request, @@ -46,7 +46,7 @@ async def get_enterprise( db: Database = Depends(get_db), user: dict = Depends(require_permission("menu:gov_data")), ): - org = db.orgs.get(org_id) + org = await db.orgs.get(org_id) if org is None or org["type"] != "enterprise": raise HTTPException(status_code=404, detail="Enterprise not found") if org["region_id"] not in user.get("scope_region_ids", []): @@ -65,7 +65,7 @@ async def gov_opc( db: Database = Depends(get_db), user: dict = Depends(require_permission("menu:gov_data")), ): - opcs = [u for u in db.users.list() if u.get("role") == "opc_member"] + opcs = [u for u in await db.users.list() if u.get("role") == "opc_member"] opcs = [u for u in opcs if _in_scope(u.get("region_id"), user)] return {"items": [ {"title": u.get("nickname") or u.get("username"), "meta": f"信用 {u.get('region_id', '')} · OPC", @@ -79,7 +79,7 @@ async def gov_carriers( db: Database = Depends(get_db), user: dict = Depends(require_permission("menu:gov_data")), ): - carriers = [o for o in db.orgs.all() if o["type"] == "carrier"] + carriers = [o for o in await db.orgs.all() if o["type"] == "carrier"] carriers = [o for o in carriers if _in_scope(o.get("region_id"), user)] return {"items": [ {"title": o["name"], "meta": f"{o.get('region_id', '')} · 载体", "tag": "载体"} @@ -93,7 +93,7 @@ async def gov_data( user: dict = Depends(require_permission("menu:gov_data")), ): scope = user.get("scope_region_ids", []) - stats = db.stats.overview(region_ids=scope) + stats = await db.stats.overview(region_ids=scope) return { "stats": [ {"key": "totalOPC", "value": stats["user_count"]}, @@ -124,7 +124,7 @@ async def gov_subsidies( db: Database = Depends(get_db), user: dict = Depends(require_permission("menu:gov_data")), ): - return {"items": db.subsidies.list(user.get("scope_region_ids", []))} + return {"items": await db.subsidies.list(user.get("scope_region_ids", []))} @router.post("/subsidies/{aid}/approve", summary="补贴审批(按级别推进)") @@ -134,7 +134,7 @@ async def gov_approve_subsidy( db: Database = Depends(get_db), user: dict = Depends(require_permission("menu:gov_data")), ): - sub = db.subsidies.get(aid) + sub = await db.subsidies.get(aid) if sub is None: raise HTTPException(status_code=404, detail="Subsidy not found") if sub.get("region_id") and sub["region_id"] not in user.get("scope_region_ids", []): @@ -142,8 +142,8 @@ async def gov_approve_subsidy( nxt = (_SUBSIDY_NEXT.get(user.get("sub_role") or "") or {}).get(sub["status"]) if not nxt: raise HTTPException(status_code=400, detail="当前级别不能审批该状态") - updated = db.subsidies.set_status(aid, nxt) - write_audit(db, action="subsidy.approve", resource="subsidy", resource_id=aid, + updated = await db.subsidies.set_status(aid, nxt) + await write_audit(db, action="subsidy.approve", resource="subsidy", resource_id=aid, detail=nxt, user=user, request=request) return updated @@ -155,12 +155,12 @@ async def gov_pay_subsidy( db: Database = Depends(get_db), user: dict = Depends(require_permission("menu:gov_data")), ): - sub = db.subsidies.get(aid) + sub = await db.subsidies.get(aid) if sub is None: raise HTTPException(status_code=404, detail="Subsidy not found") if sub["status"] != "approved": raise HTTPException(status_code=400, detail="仅已终审的补贴可发放") - updated = db.subsidies.set_status(aid, "paid") - write_audit(db, action="subsidy.pay", resource="subsidy", resource_id=aid, + updated = await db.subsidies.set_status(aid, "paid") + await write_audit(db, action="subsidy.pay", resource="subsidy", resource_id=aid, user=user, request=request) return updated diff --git a/app/routers/rbac_investor.py b/app/api/routers/rbac_investor.py similarity index 78% rename from app/routers/rbac_investor.py rename to app/api/routers/rbac_investor.py index 1b8bd08..e3c0358 100644 --- a/app/routers/rbac_investor.py +++ b/app/api/routers/rbac_investor.py @@ -12,8 +12,8 @@ from fastapi import APIRouter, Depends, HTTPException, Request from pydantic import BaseModel from ..dependencies import get_db -from ..rbac import require_roles, write_audit -from ..repositories import Database +from ...rbac import require_roles, write_audit +from ...infrastructure.repositories import Database router = APIRouter(prefix="/investor", tags=["investor"]) @@ -72,13 +72,13 @@ async def investor_dashboard( db: Database = Depends(get_db), user: dict = Depends(require_roles("investor")), ): - pref = db.investor_prefs.get(user["id"]) or {} - portfolio = db.portal_pages.get("investor", "portfolio") or {} - roadshows = [r for r in db.roadshows.list(status="published")][:3] + pref = await db.investor_prefs.get(user["id"]) or {} + portfolio = await db.portal_pages.get("investor", "portfolio") or {} + roadshows = [r for r in await db.roadshows.list(status="published")][:3] return { "preference": pref, "stats": portfolio.get("stats", []), - "intents": len(db.intents.list_for(user["id"])), + "intents": len(await db.intents.list_for(user["id"])), "recentRoadshows": roadshows, } @@ -88,7 +88,7 @@ async def get_preferences( db: Database = Depends(get_db), user: dict = Depends(require_roles("investor")), ): - return db.investor_prefs.get(user["id"]) or {} + return await db.investor_prefs.get(user["id"]) or {} @router.put("/preferences", summary="保存投资方向") @@ -98,8 +98,8 @@ async def set_preferences( db: Database = Depends(get_db), actor: dict = Depends(require_roles("investor")), ): - pref = db.investor_prefs.upsert(actor["id"], req.model_dump(exclude_none=True)) - write_audit(db, action="preference.update", resource="investor", resource_id=actor["id"], + pref = await db.investor_prefs.upsert(actor["id"], req.model_dump(exclude_none=True)) + await write_audit(db, action="preference.update", resource="investor", resource_id=actor["id"], user=actor, request=request) return pref @@ -109,7 +109,7 @@ async def list_projects( db: Database = Depends(get_db), _u: dict = Depends(require_roles("investor")), ): - return db.portal_pages.get("investor", "projects") or {"items": []} + return await db.portal_pages.get("investor", "projects") or {"items": []} @router.get("/trainings", summary="投融资培训") @@ -117,7 +117,7 @@ async def list_trainings( db: Database = Depends(get_db), _u: dict = Depends(require_roles("investor")), ): - return db.portal_pages.get("investor", "trainings") or {"items": []} + return await db.portal_pages.get("investor", "trainings") or {"items": []} @router.post("/trainings/{training_id}/enroll", summary="培训报名") @@ -127,11 +127,11 @@ async def enroll_training( db: Database = Depends(get_db), actor: dict = Depends(require_roles("investor")), ): - data = db.portal_pages.get("investor", "trainings") or {"items": []} + data = await db.portal_pages.get("investor", "trainings") or {"items": []} item = next((it for it in data.get("items", []) if it.get("title") == training_id), None) name = (item or {}).get("title", training_id) - enroll = db.training_enrolls.create(actor["id"], training_id, name) - write_audit(db, action="training.enroll", resource="training", resource_id=enroll["id"], + enroll = await db.training_enrolls.create(actor["id"], training_id, name) + await write_audit(db, action="training.enroll", resource="training", resource_id=enroll["id"], detail=name, user=actor, request=request) return enroll @@ -141,7 +141,7 @@ async def my_trainings( db: Database = Depends(get_db), user: dict = Depends(require_roles("investor")), ): - return {"items": db.training_enrolls.list_for(user["id"])} + return {"items": await db.training_enrolls.list_for(user["id"])} @router.get("/portfolio", summary="投资组合") @@ -149,7 +149,7 @@ async def portfolio( db: Database = Depends(get_db), _u: dict = Depends(require_roles("investor")), ): - return db.portal_pages.get("investor", "portfolio") or {"items": []} + return await db.portal_pages.get("investor", "portfolio") or {"items": []} @router.post("/projects/{project_id}/intent", summary="发起投资意向/约谈") @@ -160,10 +160,10 @@ async def create_intent( db: Database = Depends(get_db), actor: dict = Depends(require_roles("investor")), ): - intent = db.intents.create( + intent = await db.intents.create( actor["id"], req.project_id or project_id, req.project_name, req.message, ) - write_audit(db, action="intent.create", resource="investor", resource_id=intent["id"], + await write_audit(db, action="intent.create", resource="investor", resource_id=intent["id"], detail=req.project_name, user=actor, request=request) return intent @@ -173,7 +173,7 @@ async def list_intents( db: Database = Depends(get_db), user: dict = Depends(require_roles("investor")), ): - return {"items": db.intents.list_for(user["id"])} + return {"items": await db.intents.list_for(user["id"])} @router.get("/roadshows", summary="路演活动列表") @@ -182,7 +182,7 @@ async def list_roadshows( db: Database = Depends(get_db), _u: dict = Depends(require_roles(*_ROADSHOW_ROLES)), ): - return {"items": db.roadshows.list(status=status)} + return {"items": await db.roadshows.list(status=status)} @router.post("/roadshows", summary="发布路演活动(按权限与范围审核)") @@ -201,8 +201,8 @@ async def create_roadshow( fields = {**req.model_dump(exclude_none=True), "publisher_id": actor["id"], "publisher_role": actor.get("role", "investor"), "status": _pub_status(need_review), "need_review": need_review} - rs = db.roadshows.create(fields) - write_audit(db, action="roadshow.publish", resource="roadshow", resource_id=rs["id"], + rs = await db.roadshows.create(fields) + await write_audit(db, action="roadshow.publish", resource="roadshow", resource_id=rs["id"], detail=f"{rs['title']} need_review={need_review}", user=actor, request=request) return rs @@ -214,10 +214,10 @@ async def register_roadshow( db: Database = Depends(get_db), actor: dict = Depends(require_roles(*_ROADSHOW_ROLES)), ): - rs = db.roadshows.get(roadshow_id) + rs = await db.roadshows.get(roadshow_id) if rs is None or rs["status"] not in ("published", "registering"): raise HTTPException(status_code=400, detail="活动不可报名") if rs.get("register_deadline") and rs["register_deadline"] < "2099-01-01": pass # 演示:不做硬性截止校验 - reg = db.roadshow_regs.create(roadshow_id, actor["id"], role="investor") + reg = await db.roadshow_regs.create(roadshow_id, actor["id"], role="investor") return reg diff --git a/app/routers/rbac_opc.py b/app/api/routers/rbac_opc.py similarity index 75% rename from app/routers/rbac_opc.py rename to app/api/routers/rbac_opc.py index 5243abe..bcc25bc 100644 --- a/app/routers/rbac_opc.py +++ b/app/api/routers/rbac_opc.py @@ -10,9 +10,9 @@ from fastapi import APIRouter, Depends, HTTPException, Request from pydantic import BaseModel from ..dependencies import get_db -from ..rbac import require_roles, write_audit -from ..repositories import Database, new_id, utcnow_iso -from ..models import FinanceRecord +from ...rbac import require_roles, write_audit +from ...infrastructure.repositories import Database, new_id, utcnow_iso +from ...infrastructure.models import FinanceRecord router = APIRouter(prefix="/opc", tags=["opc"]) @@ -25,9 +25,9 @@ async def opc_dashboard( """返回当前 OPC 用户的工作台聚合数据(统计/进行中任务/智能体建议/月度收入/最新消息)。""" uid = user["id"] - profile = db.opc_profiles.get(uid) - task_counts = db.opc_tasks.counts(uid) - opc_tasks = db.opc_tasks.list_by_user(uid) + profile = await db.opc_profiles.get(uid) + task_counts = await db.opc_tasks.counts(uid) + opc_tasks = await db.opc_tasks.list_by_user(uid) # 进行中任务:进行中/紧急优先,取前 3 条 active_tasks = [ @@ -36,7 +36,7 @@ async def opc_dashboard( # 智能体建议(由服务端依据真实数据生成) policy_count = len( - [c for c in db.content.list(ctype="policy", status="published")] + [c for c in await db.content.list(ctype="policy", status="published")] ) agent_tips = [ { @@ -60,13 +60,13 @@ async def opc_dashboard( "stats": { "inProgressTasks": task_counts["in_progress"], "completedTasks": task_counts["completed"], - "totalEarnings": db.finance.total_income(uid), + "totalEarnings": await db.finance.total_income(uid), "creditScore": (profile or {}).get("credit_score", 80), }, "activeTasks": active_tasks, "agentTips": agent_tips, - "monthlyIncome": db.finance.monthly_income(uid), - "recentMessages": db.messages.recent(uid, limit=4), + "monthlyIncome": await db.finance.monthly_income(uid), + "recentMessages": await db.messages.recent(uid, limit=4), } @@ -77,7 +77,7 @@ async def opc_agent( db: Database = Depends(get_db), user: dict = Depends(require_roles("opc_member")), ): - agents = db.agents.get_by_user(user["id"], port=user.get("port") or "opc") + agents = await db.agents.get_by_user(user["id"], port=user.get("port") or "opc") default = next((a for a in agents if a.get("id", "").startswith("pine_agents_official_001")), None) return { "agent": { @@ -99,7 +99,7 @@ async def opc_task_square( db: Database = Depends(get_db), _u: dict = Depends(require_roles("opc_member")), ): - return {"items": db.tasks.list(status="published")} + return {"items": await db.tasks.list(status="published")} @router.get("/my-tasks", summary="我的任务看板") @@ -107,7 +107,7 @@ async def opc_my_tasks( db: Database = Depends(get_db), user: dict = Depends(require_roles("opc_member")), ): - return {"items": db.opc_tasks.list_by_user(user["id"])} + return {"items": await db.opc_tasks.list_by_user(user["id"])} @router.get("/services", summary="服务市场(在营服务商)") @@ -115,7 +115,7 @@ async def opc_services( db: Database = Depends(get_db), _u: dict = Depends(require_roles("opc_member")), ): - return {"items": db.providers.list(status="active")} + return {"items": await db.providers.list(status="active")} @router.get("/policy", summary="政策列表(已发布政策)") @@ -123,7 +123,7 @@ async def opc_policy( db: Database = Depends(get_db), _u: dict = Depends(require_roles("opc_member")), ): - return {"items": db.content.list(ctype="policy", status="published")} + return {"items": await db.content.list(ctype="policy", status="published")} @router.get("/finance", summary="财务流水") @@ -133,8 +133,8 @@ async def opc_finance( ): uid = user["id"] return { - "records": db.finance.list_by_user(uid), - "total_income": db.finance.total_income(uid), + "records": await db.finance.list_by_user(uid), + "total_income": await db.finance.total_income(uid), } @@ -143,7 +143,7 @@ async def opc_messages( db: Database = Depends(get_db), user: dict = Depends(require_roles("opc_member")), ): - return {"items": db.messages.list_by_user(user["id"])} + return {"items": await db.messages.list_by_user(user["id"])} @router.get("/profile", summary="个人资料") @@ -151,8 +151,8 @@ async def opc_profile( db: Database = Depends(get_db), user: dict = Depends(require_roles("opc_member")), ): - profile = db.users.to_profile(user) - profile["opc_profile"] = db.opc_profiles.get(user["id"]) + profile = await db.users.to_profile(user) + profile["opc_profile"] = await db.opc_profiles.get(user["id"]) return profile @@ -168,11 +168,11 @@ async def opc_grab( db: Database = Depends(get_db), actor: dict = Depends(require_roles("opc_member")), ): - task = db.tasks.get(task_id) + task = await db.tasks.get(task_id) if task is None or task["status"] != "published" or task["mode"] != "grab": raise HTTPException(status_code=400, detail="任务不可抢单") - updated = db.tasks.set_status(task_id, "in_progress") - write_audit(db, action="task.grab", resource="task", resource_id=task_id, + updated = await db.tasks.set_status(task_id, "in_progress") + await write_audit(db, action="task.grab", resource="task", resource_id=task_id, detail=actor.get("username"), user=actor, request=request) return updated @@ -185,12 +185,12 @@ async def opc_bid( db: Database = Depends(get_db), actor: dict = Depends(require_roles("opc_member")), ): - task = db.tasks.get(task_id) + task = await db.tasks.get(task_id) if task is None or task["status"] != "published" or task["mode"] != "bid": raise HTTPException(status_code=400, detail="任务不可投标") - bid = db.bids.create(task_id, actor["id"], actor.get("nickname") or actor["username"], + bid = await db.bids.create(task_id, actor["id"], actor.get("nickname") or actor["username"], req.quote, req.plan) - write_audit(db, action="task.bid", resource="bid", resource_id=bid["id"], + await write_audit(db, action="task.bid", resource="bid", resource_id=bid["id"], user=actor, request=request) return bid @@ -202,10 +202,10 @@ async def opc_deliver( db: Database = Depends(get_db), actor: dict = Depends(require_roles("opc_member")), ): - updated = db.tasks.set_status(task_id, "delivered") + updated = await db.tasks.set_status(task_id, "delivered") if updated is None: raise HTTPException(status_code=404, detail="Task not found") - write_audit(db, action="task.deliver", resource="task", resource_id=task_id, + await write_audit(db, action="task.deliver", resource="task", resource_id=task_id, user=actor, request=request) return updated @@ -229,12 +229,12 @@ async def opc_update_profile( profile_updates = {k: v for k, v in req.model_dump(exclude_none=True).items() if k in ("nickname", "account", "company", "room")} if profile_updates: - db.users.update_profile(actor["id"], profile_updates) + await db.users.update_profile(actor["id"], profile_updates) if req.credit_score is not None: - db.opc_profiles.upsert(actor["id"], req.credit_score) - write_audit(db, action="profile.update", resource="user", resource_id=actor["id"], + await db.opc_profiles.upsert(actor["id"], req.credit_score) + await write_audit(db, action="profile.update", resource="user", resource_id=actor["id"], user=actor, request=request) - fresh_user = db.users.get_by_id(actor["id"]) + fresh_user = await db.users.get_by_id(actor["id"]) return await opc_profile(db, fresh_user) @@ -257,9 +257,9 @@ async def opc_add_finance( amount=req.amount, date=req.date or utcnow_iso()[:10], note=req.note, created_at=utcnow_iso(), ) - db.session.add(record) - db.session.commit() - write_audit(db, action="finance.add", resource="finance", resource_id=record.id, + await db.session.add(record) + await db.session.commit() + await write_audit(db, action="finance.add", resource="finance", resource_id=record.id, user=actor, request=request) return {"id": record.id, "category": record.category, "amount": record.amount, "date": record.date, "note": record.note} @@ -270,7 +270,7 @@ async def opc_tax( db: Database = Depends(get_db), _u: dict = Depends(require_roles("opc_member")), ): - return db.portal_pages.get("opc", "tax") or {"items": []} + return await db.portal_pages.get("opc", "tax") or {"items": []} @router.get("/affairs", summary="OPC 工商/政务办事") @@ -278,4 +278,4 @@ async def opc_affairs( db: Database = Depends(get_db), _u: dict = Depends(require_roles("opc_member")), ): - return db.portal_pages.get("opc", "affairs") or {"items": []} + return await db.portal_pages.get("opc", "affairs") or {"items": []} diff --git a/app/routers/rbac_operator.py b/app/api/routers/rbac_operator.py similarity index 80% rename from app/routers/rbac_operator.py rename to app/api/routers/rbac_operator.py index e4969cd..8444cc4 100644 --- a/app/routers/rbac_operator.py +++ b/app/api/routers/rbac_operator.py @@ -9,8 +9,8 @@ from fastapi import APIRouter, Depends, HTTPException, Request from pydantic import BaseModel from ..dependencies import get_db -from ..rbac import require_permission, require_roles, write_audit -from ..repositories import Database +from ...rbac import require_permission, require_roles, write_audit +from ...infrastructure.repositories import Database router = APIRouter(prefix="/admin", tags=["admin-op"]) @@ -69,7 +69,7 @@ async def list_tasks( db: Database = Depends(get_db), _u: dict = Depends(require_roles("operator")), ): - return db.tasks.list(status=status) + return await db.tasks.list(status=status) @router.post("/tasks", summary="创建任务") @@ -79,8 +79,8 @@ async def create_task( db: Database = Depends(get_db), actor: dict = Depends(require_permission("action:task.manage")), ): - task = db.tasks.create(req.model_dump(exclude_none=True)) - write_audit(db, action="task.create", resource="task", resource_id=task["id"], + task = await db.tasks.create(req.model_dump(exclude_none=True)) + await write_audit(db, action="task.create", resource="task", resource_id=task["id"], detail=task["title"], user=actor, request=request) return task @@ -93,10 +93,10 @@ async def set_task_status( db: Database = Depends(get_db), actor: dict = Depends(require_permission("action:task.manage")), ): - if db.tasks.get(task_id) is None: + if await db.tasks.get(task_id) is None: raise HTTPException(status_code=404, detail="Task not found") - task = db.tasks.set_status(task_id, req.status) - write_audit(db, action="task.status", resource="task", resource_id=task_id, + task = await db.tasks.set_status(task_id, req.status) + await write_audit(db, action="task.status", resource="task", resource_id=task_id, detail=req.status, user=actor, request=request) return task @@ -108,7 +108,7 @@ async def list_providers( db: Database = Depends(get_db), _u: dict = Depends(require_roles("operator")), ): - return db.providers.list(status=status) + return await db.providers.list(status=status) @router.post("/providers", summary="创建服务商") @@ -118,8 +118,8 @@ async def create_provider( db: Database = Depends(get_db), actor: dict = Depends(require_permission("action:provider.manage")), ): - prov = db.providers.create(req.model_dump(exclude_none=True)) - write_audit(db, action="provider.create", resource="provider", resource_id=prov["id"], + prov = await db.providers.create(req.model_dump(exclude_none=True)) + await write_audit(db, action="provider.create", resource="provider", resource_id=prov["id"], detail=prov["name"], user=actor, request=request) return prov @@ -132,10 +132,10 @@ async def update_provider( db: Database = Depends(get_db), actor: dict = Depends(require_permission("action:provider.manage")), ): - updated = db.providers.update(provider_id, req.model_dump(exclude_none=True)) + updated = await db.providers.update(provider_id, req.model_dump(exclude_none=True)) if updated is None: raise HTTPException(status_code=404, detail="Provider not found") - write_audit(db, action="provider.update", resource="provider", resource_id=provider_id, + await write_audit(db, action="provider.update", resource="provider", resource_id=provider_id, detail=str(req.model_dump(exclude_none=True)), user=actor, request=request) return updated @@ -148,7 +148,7 @@ async def list_content( db: Database = Depends(get_db), _u: dict = Depends(require_roles("operator")), ): - return db.content.list(ctype=ctype, status=status) + return await db.content.list(ctype=ctype, status=status) @router.post("/content", summary="创建内容") @@ -158,8 +158,8 @@ async def create_content( db: Database = Depends(get_db), actor: dict = Depends(require_permission("action:content.manage")), ): - item = db.content.create(req.model_dump(exclude_none=True)) - write_audit(db, action="content.create", resource="content", resource_id=item["id"], + item = await db.content.create(req.model_dump(exclude_none=True)) + await write_audit(db, action="content.create", resource="content", resource_id=item["id"], detail=item["title"], user=actor, request=request) return item @@ -172,10 +172,10 @@ async def set_content_status( db: Database = Depends(get_db), actor: dict = Depends(require_permission("action:content.manage")), ): - item = db.content.set_status(content_id, req.status) + item = await db.content.set_status(content_id, req.status) if item is None: raise HTTPException(status_code=404, detail="Content not found") - write_audit(db, action="content.status", resource="content", resource_id=content_id, + await write_audit(db, action="content.status", resource="content", resource_id=content_id, detail=req.status, user=actor, request=request) return item @@ -186,7 +186,7 @@ async def list_config( db: Database = Depends(get_db), _u: dict = Depends(require_roles("operator")), ): - return db.config.all() + return await db.config.all() @router.put("/config/{key}", summary="更新系统配置") @@ -197,8 +197,8 @@ async def set_config( db: Database = Depends(get_db), actor: dict = Depends(require_permission("action:config.manage")), ): - cfg = db.config.set(key, req.value, req.description) - write_audit(db, action="config.update", resource="config", resource_id=key, + cfg = await db.config.set(key, req.value, req.description) + await write_audit(db, action="config.update", resource="config", resource_id=key, detail=req.value, user=actor, request=request) return cfg @@ -209,4 +209,4 @@ async def stats_overview( db: Database = Depends(get_db), _u: dict = Depends(require_roles("operator")), ): - return db.stats.overview() + return await db.stats.overview() diff --git a/app/routers/rbac_org.py b/app/api/routers/rbac_org.py similarity index 80% rename from app/routers/rbac_org.py rename to app/api/routers/rbac_org.py index 99c63a5..79d189d 100644 --- a/app/routers/rbac_org.py +++ b/app/api/routers/rbac_org.py @@ -6,8 +6,8 @@ from fastapi import APIRouter, Depends, HTTPException from pydantic import BaseModel from ..dependencies import get_db -from ..rbac import require_roles -from ..repositories import Database +from ...rbac import require_roles +from ...infrastructure.repositories import Database router = APIRouter(tags=["org"]) @@ -19,8 +19,8 @@ async def org_me( "opc_member", "carrier", "enterprise", "provider", "government", "operator", )), ): - org = db.orgs.get(user["org_id"]) if user.get("org_id") else None - region = db.regions.get(user["region_id"]) if user.get("region_id") else None + org = await db.orgs.get(user["org_id"]) if user.get("org_id") else None + region = await db.regions.get(user["region_id"]) if user.get("region_id") else None return { "user_id": user["id"], "role": user["role"], @@ -35,7 +35,7 @@ async def enterprise_profile( db: Database = Depends(get_db), user: dict = Depends(require_roles("enterprise")), ): - org = db.orgs.get(user["org_id"]) if user.get("org_id") else None + org = await db.orgs.get(user["org_id"]) if user.get("org_id") else None if org is None: raise HTTPException(status_code=404, detail="No enterprise bound") return org @@ -49,7 +49,7 @@ async def carrier_enterprises( """返回绑定到当前载体(parent_id == 载体 org_id)的企业。""" if not user.get("org_id"): return [] - return db.orgs.list_by_parent(user["org_id"]) + return await db.orgs.list_by_parent(user["org_id"]) @router.get("/provider/orders", summary="服务商订单(骨架)") @@ -69,9 +69,9 @@ async def org_members( user: dict = Depends(require_roles("enterprise", "carrier", "provider", "investor", "operator")), ): # 机构管理员本人 或 平台运营 可查看 - if user.get("role") not in ("operator",) and not db.org_members.is_admin(org_id, user["id"]): + if user.get("role") not in ("operator",) and not await db.org_members.is_admin(org_id, user["id"]): raise HTTPException(status_code=403, detail="Forbidden: not org admin") - return {"items": db.org_members.list_for_org(org_id)} + return {"items": await db.org_members.list_for_org(org_id)} class OrgMemberAdd(BaseModel): @@ -88,17 +88,17 @@ async def org_add_member( db: Database = Depends(get_db), user: dict = Depends(require_roles("enterprise", "carrier", "provider", "investor", "operator")), ): - if user.get("role") not in ("operator",) and not db.org_members.is_admin(org_id, user["id"]): + if user.get("role") not in ("operator",) and not await db.org_members.is_admin(org_id, user["id"]): raise HTTPException(status_code=403, detail="Forbidden: not org admin") target_id = req.user_id if not target_id and req.username: - found = db.users.get_by_username(req.username) + found = await db.users.get_by_username(req.username) if found is None: raise HTTPException(status_code=404, detail="账号不存在") target_id = found["id"] if not target_id: raise HTTPException(status_code=400, detail="缺少 user_id 或 username") - return db.org_members.add_member(org_id, target_id, req.role, req.is_admin) + return await db.org_members.add_member(org_id, target_id, req.role, req.is_admin) @router.get("/me/orgs", summary="我加入的机构") @@ -108,9 +108,9 @@ async def my_orgs( ): """返回当前账号挂靠的所有机构及成员角色(支持一账号多机构)。""" out = [] - for org in db.orgs.all(): + for org in await db.orgs.all(): member = None - for m in db.org_members.list_for_org(org["id"]): + for m in await db.org_members.list_for_org(org["id"]): if m["user_id"] == user["id"]: member = m break diff --git a/app/routers/rbac_portals.py b/app/api/routers/rbac_portals.py similarity index 83% rename from app/routers/rbac_portals.py rename to app/api/routers/rbac_portals.py index d76d3a5..a2087c4 100644 --- a/app/routers/rbac_portals.py +++ b/app/api/routers/rbac_portals.py @@ -10,8 +10,8 @@ from fastapi import APIRouter, Depends, HTTPException, Request from pydantic import BaseModel from ..dependencies import get_db -from ..rbac import require_roles, write_audit -from ..repositories import Database +from ...rbac import require_roles, write_audit +from ...infrastructure.repositories import Database router = APIRouter(tags=["portals"]) @@ -33,7 +33,7 @@ def _dashboard(port: str): db: Database = Depends(get_db), _u: dict = Depends(require_roles(role)), ): - payload = db.portal_dashboards.get(port) + payload = await db.portal_dashboards.get(port) if payload is None: raise HTTPException(status_code=404, detail="Dashboard not found") return payload @@ -50,7 +50,7 @@ def _page(port: str, page: str): db: Database = Depends(get_db), _u: dict = Depends(require_roles(role)), ): - payload = db.portal_pages.get(port, page) + payload = await db.portal_pages.get(port, page) if payload is None: raise HTTPException(status_code=404, detail="Page data not found") payload.setdefault("port", port) @@ -92,7 +92,7 @@ async def enterprise_tasks( db: Database = Depends(get_db), _u: dict = Depends(require_roles("enterprise")), ): - return {"items": db.tasks.list(status="published")} + return {"items": await db.tasks.list(status="published")} @router.post("/enterprise/tasks", summary="企业·发布任务") @@ -102,8 +102,8 @@ async def enterprise_create_task( db: Database = Depends(get_db), actor: dict = Depends(require_roles("enterprise")), ): - task = db.tasks.create({**req.model_dump(exclude_none=True), "status": "pending"}) - write_audit(db, action="task.publish", resource="task", resource_id=task["id"], + task = await db.tasks.create({**req.model_dump(exclude_none=True), "status": "pending"}) + await write_audit(db, action="task.publish", resource="task", resource_id=task["id"], detail=task["title"], user=actor, request=request) return task @@ -115,10 +115,10 @@ async def enterprise_submit_task( db: Database = Depends(get_db), actor: dict = Depends(require_roles("enterprise")), ): - task = db.tasks.set_status(task_id, "published") + task = await db.tasks.set_status(task_id, "published") if task is None: raise HTTPException(status_code=404, detail="Task not found") - write_audit(db, action="task.submit", resource="task", resource_id=task_id, + await write_audit(db, action="task.submit", resource="task", resource_id=task_id, user=actor, request=request) return task @@ -129,7 +129,7 @@ async def enterprise_list_bids( db: Database = Depends(get_db), _u: dict = Depends(require_roles("enterprise")), ): - return {"items": db.bids.list_for_task(task_id)} + return {"items": await db.bids.list_for_task(task_id)} @router.post("/enterprise/tasks/{task_id}/bids/{bid_id}/win", summary="企业·评标中标") @@ -139,11 +139,11 @@ async def enterprise_win_bid( db: Database = Depends(get_db), actor: dict = Depends(require_roles("enterprise")), ): - bid = db.bids.set_status(bid_id, "win") + bid = await db.bids.set_status(bid_id, "win") if bid is None: raise HTTPException(status_code=404, detail="Bid not found") - db.tasks.set_status(task_id, "in_progress") - write_audit(db, action="bid.win", resource="bid", resource_id=bid_id, + await db.tasks.set_status(task_id, "in_progress") + await write_audit(db, action="bid.win", resource="bid", resource_id=bid_id, detail=f"task={task_id} winner={bid['opc_name']}", user=actor, request=request) return bid @@ -161,12 +161,12 @@ async def enterprise_review_task( db: Database = Depends(get_db), actor: dict = Depends(require_roles("enterprise")), ): - task = db.tasks.get(task_id) + task = await db.tasks.get(task_id) if task is None: raise HTTPException(status_code=404, detail="Task not found") new_status = "completed" if req.action == "accept" else "in_progress" - updated = db.tasks.set_status(task_id, new_status) - write_audit(db, action="task.review", resource="task", resource_id=task_id, + updated = await db.tasks.set_status(task_id, new_status) + await write_audit(db, action="task.review", resource="task", resource_id=task_id, detail=req.action, user=actor, request=request) return updated @@ -184,7 +184,7 @@ async def carrier_referrals( db: Database = Depends(get_db), user: dict = Depends(require_roles("carrier")), ): - return {"items": db.referrals.list_for(user.get("org_id"))} + return {"items": await db.referrals.list_for(user.get("org_id"))} @router.post("/carrier/referrals", summary="载体·引荐服务商给园区内 OPC") @@ -194,9 +194,9 @@ async def carrier_create_referral( db: Database = Depends(get_db), actor: dict = Depends(require_roles("carrier")), ): - referral = db.referrals.create({ + referral = await db.referrals.create({ **req.model_dump(exclude_none=True), "carrier_id": actor.get("org_id") or actor["id"], }) - write_audit(db, action="referral.create", resource="referral", resource_id=referral["id"], + await write_audit(db, action="referral.create", resource="referral", resource_id=referral["id"], detail=f"{referral['provider_name']}->{referral['opc_name']}", user=actor, request=request) return referral diff --git a/app/routers/templates.py b/app/api/routers/templates.py similarity index 98% rename from app/routers/templates.py rename to app/api/routers/templates.py index 0da1e45..ae15f6e 100644 --- a/app/routers/templates.py +++ b/app/api/routers/templates.py @@ -10,7 +10,7 @@ from pathlib import Path from fastapi import APIRouter, Depends -from .. import config +from ... import config from ..dependencies import get_current_user router = APIRouter(prefix="/agent-templates", tags=["agent-templates"]) diff --git a/app/config.py b/app/config.py index c138f88..269e583 100644 --- a/app/config.py +++ b/app/config.py @@ -24,12 +24,23 @@ DATA_DIR = Path( os.environ.get("PINEAGENTS_DEMO_DATA_DIR", str(BASE_DIR / "data")), ).resolve() -# SQLite 数据库文件(auth/RBAC 域)。可用环境变量覆盖,测试用 tmp 路径。 +# 数据库连接(异步)。开发默认 SQLite+aiosqlite;生产用 MySQL+asyncmy(环境变量覆盖)。 +# 示例 MySQL:PINEAGENTS_DEMO_DATABASE_URL=mysql+asyncmy://user:pass@127.0.0.1:3306/opc?charset=utf8mb4 DATABASE_URL = os.environ.get( "PINEAGENTS_DEMO_DATABASE_URL", - f"sqlite:///{DATA_DIR / 'app.db'}", + f"sqlite+aiosqlite:///{DATA_DIR / 'app.db'}", ) +# Redis(缓存/会话/限流/分布式锁),空则降级内存。 +REDIS_URL = os.environ.get("REDIS_URL", "redis://127.0.0.1:6379/0") + +# OSS 对象存储(aioboto3,兼容阿里云 OSS / MinIO);空则文件落本地上传目录。 +OSS_ENDPOINT = os.environ.get("OSS_ENDPOINT", "") +OSS_ACCESS_KEY = os.environ.get("OSS_ACCESS_KEY", "") +OSS_SECRET_KEY = os.environ.get("OSS_SECRET_KEY", "") +OSS_BUCKET = os.environ.get("OSS_BUCKET", "opc-files") + + USERS_FILE = DATA_DIR / "users.json" TOKENS_FILE = DATA_DIR / "tokens.json" AGENTS_FILE = DATA_DIR / "agents.json" diff --git a/app/db.py b/app/db.py index aa9eaf2..46f1e1b 100644 --- a/app/db.py +++ b/app/db.py @@ -34,14 +34,14 @@ SessionLocal = sessionmaker( def init_db() -> None: """幂等地创建全部表(create_all 对已存在表无操作)。""" # 延迟导入模型以确保表注册到 Base.metadata - from . import models # noqa: F401 + from .infrastructure import models # noqa: F401 Base.metadata.create_all(bind=engine) def create_all(engine) -> None: """在指定 engine 上创建全部表(供测试/独立实例使用)。""" - from . import models # noqa: F401 + from .infrastructure import models # noqa: F401 Base.metadata.create_all(bind=engine) diff --git a/app/infrastructure/__init__.py b/app/infrastructure/__init__.py new file mode 100644 index 0000000..914e152 --- /dev/null +++ b/app/infrastructure/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +"""基础设施层(含数据模型层):DB/缓存/OSS/安全/种子。禁止反向依赖上层。""" diff --git a/app/infrastructure/cache.py b/app/infrastructure/cache.py new file mode 100644 index 0000000..31a600e --- /dev/null +++ b/app/infrastructure/cache.py @@ -0,0 +1,60 @@ +# -*- coding: utf-8 -*- +"""基础设施层 · Redis 异步封装(缓存 / 限流 / 分布式锁)。""" +from __future__ import annotations + +import asyncio +from typing import Any, Optional + +from redis.asyncio import Redis + +from .. import config + + +class Cache: + """redis.asyncio 封装:连接池 / get / set / incr / 分布式锁(SET NX EX)。""" + + def __init__(self, url: str | None = None) -> None: + self._redis: Optional[Redis] = None + self._url = url or config.REDIS_URL + + async def connect(self) -> None: + if self._redis is None: + self._redis = Redis.from_url(self._url, decode_responses=True) + + async def close(self) -> None: + if self._redis is not None: + await self._redis.aclose() + self._redis = None + + async def get(self, key: str) -> str | None: + if self._redis is None: + return None + return await self._redis.get(key) + + async def set(self, key: str, value: str, ttl: int | None = None) -> None: + if self._redis is None: + return + await self._redis.set(key, value, ex=ttl) + + async def delete(self, key: str) -> None: + if self._redis is None: + return + await self._redis.delete(key) + + async def incr(self, key: str) -> int: + if self._redis is None: + return 0 + return int(await self._redis.incr(key)) + + async def lock(self, key: str, ttl: int = 30) -> bool: + """分布式锁:SET key token NX EX ttl(成功拿到锁返回 True)。""" + if self._redis is None: + return True # 无 Redis 时降级为总是通过(单机部署) + token = f"{asyncio.get_event_loop().time()}" + ok = await self._redis.set(key, token, nx=True, ex=ttl) + return bool(ok) + + async def unlock(self, key: str) -> None: + if self._redis is None: + return + await self._redis.delete(key) diff --git a/app/infrastructure/db.py b/app/infrastructure/db.py new file mode 100644 index 0000000..a79ebc4 --- /dev/null +++ b/app/infrastructure/db.py @@ -0,0 +1,65 @@ +# -*- coding: utf-8 -*- +"""基础设施层 · 异步 SQLAlchemy 引擎 / 会话 / 声明基类。 + +四层架构:基础设施层(含数据模型层)。全异步: + - 开发/默认:SQLite + aiosqlite + - 生产:MySQL + asyncmy(经 config.DATABASE_URL 切换) +每请求一个 async session(``get_session`` 依赖),事件循环内无阻塞。 +""" +from __future__ import annotations + +from collections.abc import AsyncGenerator + +from sqlalchemy.ext.asyncio import ( + AsyncSession, + async_sessionmaker, + create_async_engine, +) +from sqlalchemy.orm import DeclarativeBase + +from .. import config + + +class Base(DeclarativeBase): + """SQLAlchemy 声明式基类(数据模型层继承)。""" + + +engine = create_async_engine( + config.DATABASE_URL, + pool_pre_ping=True, +) + +AsyncSessionLocal = async_sessionmaker( + bind=engine, + class_=AsyncSession, + autoflush=False, + expire_on_commit=False, +) + + +async def init_db() -> None: + """幂等地创建全部表(create_all 对已存在表无操作)。""" + from . import models # noqa: F401 + + async with engine.begin() as conn: + await conn.run_sync(Base.metadata.create_all) + + +async def get_session() -> AsyncGenerator[AsyncSession, None]: + """FastAPI 依赖:请求级异步 SQLAlchemy 会话,请求结束自动关闭。""" + async with AsyncSessionLocal() as session: + yield session + + +def make_async_engine(db_url: str): + """按给定 URL 创建独立异步引擎(测试用 tmp 数据库)。""" + return create_async_engine(db_url, pool_pre_ping=True) + + +def make_async_session_factory(db_url: str) -> async_sessionmaker: + return async_sessionmaker( + bind=make_async_engine(db_url), + class_=AsyncSession, + autoflush=False, + expire_on_commit=False, + ) diff --git a/app/models.py b/app/infrastructure/models.py similarity index 100% rename from app/models.py rename to app/infrastructure/models.py diff --git a/app/infrastructure/oss.py b/app/infrastructure/oss.py new file mode 100644 index 0000000..23872f4 --- /dev/null +++ b/app/infrastructure/oss.py @@ -0,0 +1,63 @@ +# -*- coding: utf-8 -*- +"""基础设施层 · OSS 对象存储异步封装(aioboto3,兼容阿里云 OSS / MinIO)。 + +未配置 OSS 时降级为本地上传目录(uploads/),保证无 OSS 也可用。 +""" +from __future__ import annotations + +import os +from pathlib import Path +from typing import Optional + +from .. import config + + +class OSS: + """对象存储:上传文件 / 生成访问 URL。配置了 OSS_ENDPOINT 才启用,否则落本地上传目录。""" + + def __init__(self) -> None: + self._client: Optional[object] = None + self.enabled = bool(config.OSS_ENDPOINT) + self.bucket = config.OSS_BUCKET + self.local_dir = Path(__file__).resolve().parent.parent.parent / "uploads" + self.local_dir.mkdir(parents=True, exist_ok=True) + + async def _get_client(self): + if self._client is None: + import aioboto3 + + session = aioboto3.Session( + aws_access_key_id=config.OSS_ACCESS_KEY, + aws_secret_access_key=config.OSS_SECRET_KEY, + ) + self._client = session.client( + "s3", + endpoint_url=config.OSS_ENDPOINT, + region_name="oss-cn-hangzhou", + ) + return self._client + + async def upload(self, key: str, data: bytes, content_type: str = "application/octet-stream") -> str: + """上传对象,返回可访问的相对 URL。""" + if self.enabled: + client = await self._get_client() + await client.put_object(Bucket=self.bucket, Key=key, Body=data, ContentType=content_type) + return f"/oss/{key}" + # 本地降级 + dest = self.local_dir / key + dest.parent.mkdir(parents=True, exist_ok=True) + dest.write_bytes(data) + return f"/uploads/{key}" + + async def presigned_url(self, key: str, expires: int = 3600) -> str: + """生成临时访问 URL(OSS 启用时)。""" + if self.enabled: + client = await self._get_client() + return await client.generate_presigned_url( + "get_object", Params={"Bucket": self.bucket, "Key": key}, ExpiresIn=expires + ) + return f"/uploads/{key}" + + @staticmethod + def is_configured() -> bool: + return bool(config.OSS_ENDPOINT) diff --git a/app/infrastructure/repositories.py b/app/infrastructure/repositories.py new file mode 100644 index 0000000..b001643 --- /dev/null +++ b/app/infrastructure/repositories.py @@ -0,0 +1,1804 @@ +# -*- coding: utf-8 -*- +"""数据访问层(Repository):SQLAlchemy/SQLite 实现。 + +每个 Repository 对应一张表,只做行级操作,不掺入 HTTP/路由逻辑。 +方法签名与原先 JSON 实现保持一致,路由与业务逻辑不变。 +""" +from __future__ import annotations + +import json +import secrets +from datetime import datetime, timezone + +from sqlalchemy import delete, select +from sqlalchemy.ext.asyncio import AsyncSession + +from .. import config +from .models import ( + Agent, + AuditLog, + Bid, + ContentItem, + Contract, + Dispute, + Escrow, + FinanceRecord, + InvestmentIntent, + InvestorPreference, + Message, + Notification, + OpcProfile, + OpcTask, + Organization, + OrganizationMember, + Permission, + PortalDashboard, + PortalPage, + Rating, + Region, + Roadshow, + RoadshowRegistration, + Role, + RolePermission, + ServiceProvider, + ServiceReferral, + SessionToken, + SubsidyApplication, + SystemConfig, + Task, + TrainingEnrollment, + User, + UserIdentity, +) +from .security import hash_password, verify_password + +USER_TABLE = "users" +TOKEN_TABLE = "tokens" +AGENTS_TABLE = "agents" + +# 内置智能体身份(唯一来源在服务端;本地据此同步 workspace) +DEFAULT_AGENT_ID = "pine_agents_official_001" +QA_AGENT_ID = "pine_agents_official_002" +AGENT_SEED = ( + { + "id": DEFAULT_AGENT_ID, + "name": "小园", + "description": "默认助手,处理和园区、创业、政策等相关工作", + "language": "zh", + "model_name": "", + "template_type": "default", + "deletable": False, + "use_fixed_soul": True, + }, + { + "id": QA_AGENT_ID, + "name": "问答助手", + "description": ( + "内置 PineAgents 设置问答助手,本地配置在 `PINEAGENTS_WORKING_DIR` 下," + "并提供文档。建议在回答前阅读文件;在此工作区外使用绝对路径编写代码。" + ), + "language": "zh", + "model_name": "", + "template_type": "qa", + "deletable": True, + "use_fixed_soul": False, + }, +) + +PROFILE_FIELDS = ( + "username", + "nickname", + "account", + "company", + "room", + "avatar", + "company_avatar", +) + + +# --------------------------------------------------------------------------- +# 记录工具 +# --------------------------------------------------------------------------- +def utcnow_iso() -> str: + """当前 UTC 时间,ISO 8601 字符串(如 ``2026-08-02T12:00:00+00:00``)。""" + return datetime.now(timezone.utc).isoformat() + + +def new_id(prefix: str) -> str: + """生成记录主键,如 ``u_`` / ``sess_``。""" + return f"{prefix}_{secrets.token_hex(12)}" + + +# 业务角色 → 端口映射(与 rbac_admin._port_for_role 保持一致) +_PORT_FOR_ROLE = { + "opc_member": "opc", "carrier": "carrier", "enterprise": "enterprise", + "provider": "provider", "government": "government", "operator": "operator", + "investor": "investor", "developer": "developer", +} + + +def is_expired(expires_at: str) -> bool: + """判断 ``expires_at``(ISO 字符串)是否已过期。""" + try: + return datetime.fromisoformat(expires_at) < datetime.now(timezone.utc) + except (ValueError, TypeError): + return True + + +def _iso_in(seconds: int) -> str: + """返回当前时刻往后 ``seconds`` 秒的 ISO 时间戳。""" + return datetime.fromtimestamp( + datetime.now(timezone.utc).timestamp() + seconds, + tz=timezone.utc, + ).isoformat() + + +def _user_to_dict(u: User) -> dict: + return { + "id": u.id, + "username": u.username, + "password_hash": u.password_hash, + "password_salt": u.password_salt, + "nickname": u.nickname, + "account": u.account, + "company": u.company, + "room": u.room, + "avatar": u.avatar, + "company_avatar": u.company_avatar, + "role": u.role, + "sub_role": u.sub_role, + "org_id": u.org_id, + "region_id": u.region_id, + "status": u.status, + "token_version": u.token_version, + "created_at": u.created_at, + "updated_at": u.updated_at, + } + + +# --------------------------------------------------------------------------- +# 用户表 +# --------------------------------------------------------------------------- +class UserRepository: + def __init__(self, session: AsyncSession): + self.session = session + + async def has_users(self) -> bool: + return await self.session.scalar(select(User.id).limit(1)) is not None + + async def get_by_username(self, username: str) -> dict | None: + u = await self.session.scalar( + select(User).where(User.username.ilike(username.strip())), + ) + return _user_to_dict(u) if u else None + + async def get_by_id(self, user_id: str) -> dict | None: + u = await self.session.get(User, user_id) + return _user_to_dict(u) if u else None + + async def list(self) -> list[dict]: + return [_user_to_dict(u) for u in await self.session.scalars(select(User).order_by(User.created_at))] + + async def create( + self, + username: str, + password: str, + *, + nickname: str = "", + account: str = "", + company: str = "", + room: str = "", + avatar: str = "", + company_avatar: str = "", + role: str = "opc_member", + sub_role: str | None = None, + org_id: str | None = None, + region_id: str | None = None, + ) -> dict: + digest, salt = hash_password(password) + now = utcnow_iso() + u = User( + id=new_id("u"), + username=username.strip(), + password_hash=digest, + password_salt=salt, + nickname=nickname, + account=account, + company=company, + room=room, + avatar=avatar, + company_avatar=company_avatar, + role=role, + sub_role=sub_role, + org_id=org_id, + region_id=region_id, + status="active", + token_version=0, + created_at=now, + updated_at=now, + ) + self.session.add(u) + await self.session.commit() + return _user_to_dict(u) + + async def update_profile(self, user_id: str, fields: dict) -> dict | None: + u = await self.session.get(User, user_id) + if u is None: + return None + allowed = {k: fields[k] for k in fields if k in PROFILE_FIELDS} + if not allowed: + return None + for k, v in allowed.items(): + setattr(u, k, v) + u.updated_at = utcnow_iso() + await self.session.commit() + return _user_to_dict(u) + + async def update_credentials( + self, + user_id: str, + new_username: str | None, + new_password: str | None, + ) -> dict | None: + u = await self.session.get(User, user_id) + if u is None: + return None + if new_username is not None: + u.username = new_username.strip() + if new_password is not None: + digest, salt = hash_password(new_password) + u.password_hash = digest + u.password_salt = salt + u.updated_at = utcnow_iso() + await self.session.commit() + return _user_to_dict(u) + + async def verify_password(self, user: dict, password: str) -> bool: + return verify_password( + password, + user.get("password_hash", ""), + user.get("password_salt", ""), + ) + + async def set_role( + self, + user_id: str, + role: str, + sub_role: str | None, + org_id: str | None, + region_id: str | None, + ) -> dict | None: + u = await self.session.get(User, user_id) + if u is None: + return None + # 同步该用户"主角色"身份(角色改变需对已绑定身份会话生效) + new_port = _PORT_FOR_ROLE.get(role, "opc") + for ident in await self.session.scalars( + select(UserIdentity).where(UserIdentity.user_id == user_id) + ): + if ident.role == u.role: + ident.role = role + ident.sub_role = sub_role + ident.port = new_port + ident.org_id = org_id + ident.region_id = region_id + ident.updated_at = utcnow_iso() + u.role = role + u.sub_role = sub_role + u.org_id = org_id + u.region_id = region_id + u.token_version += 1 # 使既有令牌失效,角色变更即时生效 + u.updated_at = utcnow_iso() + await self.session.commit() + return _user_to_dict(u) + + async def set_status(self, user_id: str, status: str) -> dict | None: + u = await self.session.get(User, user_id) + if u is None: + return None + u.status = status + u.updated_at = utcnow_iso() + await self.session.commit() + return _user_to_dict(u) + + async def bump_token_version(self, user_id: str) -> int: + u = await self.session.get(User, user_id) + if u is None: + return 0 + u.token_version += 1 + u.updated_at = utcnow_iso() + await self.session.commit() + return u.token_version + + async def to_profile(self, user: dict) -> dict: + """把用户记录裁剪成对外暴露的资料结构(不含任何密码字段)。""" + profile = {field: user.get(field, "") for field in PROFILE_FIELDS} + profile["role"] = user.get("role", "opc_member") + profile["sub_role"] = user.get("sub_role") + profile["org_id"] = user.get("org_id") + profile["region_id"] = user.get("region_id") + profile["permissions"] = user.get("permissions", []) + profile["scope_region_ids"] = user.get("scope_region_ids", []) + profile["identity_id"] = user.get("identity_id") + profile["port"] = user.get("port") + profile["identity_name"] = user.get("identity_name", "") + return profile + + +# --------------------------------------------------------------------------- +# 令牌表(JWT + sessions 撤销台账) +# --------------------------------------------------------------------------- +class TokenRepository: + def __init__(self, session: AsyncSession): + self.session = session + + async def create( + self, + user: dict, + *, + permissions: list[str], + scope_region_ids: list[str], + scope_level: str | None, + expiry_seconds: int, + identity_id: str | None = None, + ) -> dict: + """签发 JWT 并登记 sessions 行,返回 {token, id, user_id, ...}。""" + from ..jwt import create_access_token + + token, jti, expires_at = create_access_token( + user, + permissions=permissions, + scope_region_ids=scope_region_ids, + scope_level=scope_level, + expiry_seconds=expiry_seconds, + identity_id=identity_id, + ) + now = utcnow_iso() + rec = SessionToken( + id=jti, + jti=jti, + token=token, + user_id=user["id"], + username=user.get("username", ""), + created_at=now, + expires_at=expires_at, + revoked=False, + ) + self.session.add(rec) + await self.session.commit() + return {"id": jti, "token": token, "user_id": user["id"]} + + async def get(self, token: str) -> dict | None: + """按 JWT 解码得到的 jti 查会话行。""" + from ..jwt import decode_access_token + + payload = decode_access_token(token) + if not payload: + return None + rec = await self.session.scalar( + select(SessionToken).where(SessionToken.jti == payload.get("jti")), + ) + if rec is None: + return None + return { + "id": rec.id, + "jti": rec.jti, + "token": rec.token, + "user_id": rec.user_id, + "username": rec.username, + "expires_at": rec.expires_at, + "revoked": rec.revoked, + } + + async def session_valid(self, jti: str, user_id: str, ver: int) -> bool: + """JWT 声称的会话与用户版本是否仍有效。""" + rec = await self.session.scalar( + select(SessionToken).where(SessionToken.jti == jti), + ) + if rec is None or rec.revoked or rec.user_id != user_id: + return False + if is_expired(rec.expires_at): + return False + u = await self.session.get(User, user_id) + return u is not None and u.token_version == ver and u.status == "active" + + async def get_valid_user_id(self, token: str) -> str | None: + """JWT 有效(验签、会话未吊销、版本一致、用户启用)时返回 user_id。""" + from ..jwt import decode_access_token + + payload = decode_access_token(token) + if not payload: + return None + user_id = payload.get("sub") + if not self.session_valid(payload.get("jti", ""), user_id, payload.get("ver", 0)): + return None + return user_id + + async def revoke(self, token: str) -> bool: + rec = await self.get(token) + if rec is None: + return False + row = await self.session.get(SessionToken, rec["id"]) + if row is None: + return False + row.revoked = True + await self.session.commit() + return True + + async def revoke_all(self, user_id: str | None = None) -> int: + """吊销会话(指定用户或全部),并递增其 token_version 使旧 JWT 失效。""" + if user_id is not None: + affected = (await self.session.execute( + delete(SessionToken).where(SessionToken.user_id == user_id) + )).rowcount + u = await self.session.get(User, user_id) + if u is not None: + u.token_version += 1 + await self.session.commit() + return affected + affected = (await self.session.execute(delete(SessionToken))).rowcount + for u in await self.session.scalars(select(User)): + u.token_version += 1 + await self.session.commit() + return affected + + +# --------------------------------------------------------------------------- +# 角色 / 权限 +# --------------------------------------------------------------------------- +class RoleRepository: + def __init__(self, session: AsyncSession): + self.session = session + + async def list_roles(self) -> list[dict]: + matrix = await self.role_matrix() + return [ + { + "id": r.id, "name": r.name, "scope_type": r.scope_type, + "description": r.description, "permissions": matrix.get(r.id, []), + } + for r in await self.session.scalars(select(Role).order_by(Role.scope_type, Role.id)) + ] + + async def list_permissions(self) -> list[dict]: + return [ + {"id": p.id, "name": p.name, "category": p.category, "module": p.module} + for p in await self.session.scalars(select(Permission).order_by(Permission.module, Permission.id)) + ] + + async def permissions_for(self, role: str, sub_role: str | None) -> list[str]: + """返回该用户(业务角色 + 可选子角色)的全部权限码。 + + 合并 role 与 ``role|sub_role`` 两个角色的权限。 + """ + ids = [role] + if sub_role: + ids.append(f"{role}|{sub_role}") + return sorted( + { + pid + for pid, in (await self.session.execute( + select(RolePermission.permission_id).where( + RolePermission.role_id.in_(ids) + ) + )) + } + ) + + async def role_matrix(self) -> dict[str, list[str]]: + rows = (await self.session.execute( + select(RolePermission.role_id, RolePermission.permission_id) + )).all() + matrix: dict[str, list[str]] = {} + for role_id, perm_id in rows: + matrix.setdefault(role_id, []).append(perm_id) + return matrix + + async def role_exists(self, role_id: str) -> bool: + return await self.session.get(Role, role_id) is not None + + async def set_role_permissions(self, role_id: str, permissions: list[str]) -> list[str]: + """重设某角色的权限集合(角色权限配置,仅超管)。""" + (await self.session.execute( + delete(RolePermission).where(RolePermission.role_id == role_id) + )) + for pid in permissions: + if await self.session.get(Permission, pid) is not None: + self.session.add(RolePermission(role_id=role_id, permission_id=pid)) + await self.session.commit() + return await self.permissions_for_role(role_id) + + async def permissions_for_role(self, role_id: str) -> list[str]: + if "|" in role_id: + role, sub = role_id.split("|", 1) + return await self.permissions_for(role, sub) + return await self.permissions_for(role_id, None) + + +# --------------------------------------------------------------------------- +# 区域(数据范围层级) +# --------------------------------------------------------------------------- +class RegionRepository: + def __init__(self, session: AsyncSession): + self.session = session + + async def get(self, region_id: str) -> dict | None: + r = await self.session.get(Region, region_id) + return {"id": r.id, "name": r.name, "level": r.level, "parent_id": r.parent_id} if r else None + + async def all(self) -> list[dict]: + return [ + {"id": r.id, "name": r.name, "level": r.level, "parent_id": r.parent_id} + for r in await self.session.scalars(select(Region)) + ] + + async def level(self, region_id: str) -> str | None: + r = await self.session.get(Region, region_id) + return r.level if r else None + + async def _descendants(self, region_id: str) -> set[str]: + """收集某区域的全部后代 id(含自身)。""" + result = {region_id} + rows = (await self.session.execute( + select(Region.id, Region.parent_id) + )).all() + children: dict[str, list[str]] = {} + for rid, parent in rows: + if parent: + children.setdefault(parent, []).append(rid) + stack = list(children.get(region_id, [])) + while stack: + cur = stack.pop() + if cur in result: + continue + result.add(cur) + stack.extend(children.get(cur, [])) + return result + + async def visible_region_ids(self, region_id: str | None) -> list[str]: + """数据范围:本域 + 全部后代(上级可看下级)。无区域则返回空。""" + if not region_id: + return [] + return sorted(await self._descendants(region_id)) + + +# --------------------------------------------------------------------------- +# 组织(企业 / 载体 / 服务商) +# --------------------------------------------------------------------------- +class OrgRepository: + def __init__(self, session: AsyncSession): + self.session = session + + def _to_dict(self, o: Organization) -> dict: + return { + "id": o.id, + "name": o.name, + "type": o.type, + "region_id": o.region_id, + "parent_id": o.parent_id, + "status": o.status, + "created_at": o.created_at, + } + + async def get(self, org_id: str) -> dict | None: + o = await self.session.get(Organization, org_id) + return self._to_dict(o) if o else None + + async def all(self) -> list[dict]: + return [self._to_dict(o) for o in await self.session.scalars(select(Organization))] + + async def list_by_region(self, region_ids: list[str]) -> list[dict]: + if not region_ids: + return [] + return [ + self._to_dict(o) + for o in await self.session.scalars( + select(Organization).where(Organization.region_id.in_(region_ids)) + ) + ] + + async def list_by_parent(self, parent_id: str) -> list[dict]: + return [ + self._to_dict(o) + for o in await self.session.scalars( + select(Organization).where(Organization.parent_id == parent_id) + ) + ] + + +# --------------------------------------------------------------------------- +# 审计日志 +# --------------------------------------------------------------------------- +class AuditRepository: + def __init__(self, session: AsyncSession): + self.session = session + + async def add(self, *, action: str, resource: str, resource_id: str = "", + detail: str = "", ip: str = "", user_id: str | None = None) -> None: + self.session.add( + AuditLog( + id=new_id("aud"), + user_id=user_id, + action=action, + resource=resource, + resource_id=resource_id, + detail=detail, + ip=ip, + created_at=utcnow_iso(), + ) + ) + await self.session.commit() + + async def list(self, limit: int = 100, offset: int = 0) -> list[dict]: + rows = await self.session.scalars( + select(AuditLog).order_by(AuditLog.created_at.desc()).limit(limit).offset(offset) + ) + return [ + { + "id": a.id, + "user_id": a.user_id, + "action": a.action, + "resource": a.resource, + "resource_id": a.resource_id, + "detail": a.detail, + "ip": a.ip, + "created_at": a.created_at, + } + for a in rows + ] + + +# --------------------------------------------------------------------------- +# 智能体表(身份记录;按用户隔离) +# --------------------------------------------------------------------------- +class AgentRepository: + def __init__(self, session: AsyncSession): + self.session = session + + def _to_dict(self, a: Agent) -> dict: + return { + "id": a.id, + "user_id": a.user_id, + "port": a.port, + "name": a.name, + "description": a.description, + "language": a.language, + "model_name": a.model_name, + "deletable": a.deletable, + "use_fixed_soul": a.use_fixed_soul, + "created_at": a.created_at, + "updated_at": a.updated_at, + } + + async def get_by_user(self, user_id: str, port: str | None = None) -> list[dict]: + if not port: + return [] + stmt = select(Agent).where(Agent.user_id == user_id, Agent.port == port) + rows = await self.session.scalars(stmt.order_by(Agent.created_at)) + return [self._to_dict(a) for a in rows] + + async def get(self, agent_id: str, user_id: str, port: str | None = None) -> dict | None: + if not port: + return None + stmt = select(Agent).where(Agent.id == agent_id, Agent.user_id == user_id, Agent.port == port) + a = await self.session.scalar(stmt) + return self._to_dict(a) if a else None + + async def count_by_user(self, user_id: str, port: str | None = None) -> int: + if not port: + return 0 + stmt = select(Agent.id).where(Agent.user_id == user_id, Agent.port == port) + return len(list(await self.session.scalars(stmt))) + + async def create( + self, + user_id: str, + name: str, + *, + description: str = "", + language: str = "zh", + model_name: str = "", + agent_id: str | None = None, + port: str = "opc", + ) -> dict: + now = utcnow_iso() + a = Agent( + id=agent_id or new_id("agent"), + user_id=user_id, + port=port, + name=name.strip(), + description=description, + language=language, + model_name=model_name, + deletable=True, + use_fixed_soul=False, + created_at=now, + updated_at=now, + ) + self.session.add(a) + await self.session.commit() + return self._to_dict(a) + + async def update(self, agent_id: str, user_id: str, fields: dict, port: str | None = None) -> dict | None: + if not port: + return None + stmt = select(Agent).where(Agent.id == agent_id, Agent.user_id == user_id, Agent.port == port) + a = await self.session.scalar(stmt) + if a is None: + return None + allowed = { + k: fields[k] + for k in fields + if k in ("name", "description", "language", "model_name", "use_fixed_soul") + } + if not allowed: + return None + for k, v in allowed.items(): + setattr(a, k, v) + a.updated_at = utcnow_iso() + await self.session.commit() + return self._to_dict(a) + + async def delete(self, agent_id: str, user_id: str, port: str | None = None) -> bool: + if not port: + return False + stmt = select(Agent).where(Agent.id == agent_id, Agent.user_id == user_id, Agent.port == port) + a = await self.session.scalar(stmt) + if a is None: + return False + self.session.delete(a) + await self.session.commit() + return True + + +# --------------------------------------------------------------------------- +# 任务 +# --------------------------------------------------------------------------- +class TaskRepository: + def __init__(self, session: AsyncSession): + self.session = session + + def _to_dict(self, t: Task) -> dict: + return { + "id": t.id, "title": t.title, "category": t.category, + "sub_category": t.sub_category, "description": t.description, + "mode": t.mode, "budget_min": t.budget_min, "budget_max": t.budget_max, + "deadline": t.deadline, "status": t.status, + "publisher_org_id": t.publisher_org_id, "publisher_name": t.publisher_name, + "created_at": t.created_at, "updated_at": t.updated_at, + } + + async def list(self, status: str | None = None) -> list[dict]: + q = select(Task).order_by(Task.created_at.desc()) + if status: + q = q.where(Task.status == status) + return [self._to_dict(t) for t in await self.session.scalars(q)] + + async def get(self, task_id: str) -> dict | None: + t = await self.session.get(Task, task_id) + return self._to_dict(t) if t else None + + async def create(self, fields: dict) -> dict: + now = utcnow_iso() + t = Task(id=new_id("task"), title=fields.get("title", ""), + category=fields.get("category", ""), sub_category=fields.get("sub_category", ""), + description=fields.get("description", ""), mode=fields.get("mode", "grab"), + budget_min=fields.get("budget_min", 0), budget_max=fields.get("budget_max", 0), + deadline=fields.get("deadline", ""), status=fields.get("status", "draft"), + publisher_org_id=fields.get("publisher_org_id"), + publisher_name=fields.get("publisher_name", ""), + created_at=now, updated_at=now) + self.session.add(t) + await self.session.commit() + return self._to_dict(t) + + async def set_status(self, task_id: str, status: str) -> dict | None: + t = await self.session.get(Task, task_id) + if t is None: + return None + t.status = status + t.updated_at = utcnow_iso() + await self.session.commit() + return self._to_dict(t) + + +# --------------------------------------------------------------------------- +# 服务商 +# --------------------------------------------------------------------------- +class ProviderRepository: + def __init__(self, session: AsyncSession): + self.session = session + + def _to_dict(self, p: ServiceProvider) -> dict: + return { + "id": p.id, "name": p.name, "category": p.category, + "org_id": p.org_id, "level": p.level, "rating": p.rating, + "order_count": p.order_count, "status": p.status, + "contact": p.contact, "created_at": p.created_at, "updated_at": p.updated_at, + } + + async def list(self, status: str | None = None) -> list[dict]: + q = select(ServiceProvider).order_by(ServiceProvider.created_at.desc()) + if status: + q = q.where(ServiceProvider.status == status) + return [self._to_dict(p) for p in await self.session.scalars(q)] + + async def create(self, fields: dict) -> dict: + now = utcnow_iso() + p = ServiceProvider(id=new_id("prov"), name=fields.get("name", ""), + category=fields.get("category", ""), org_id=fields.get("org_id"), + level=fields.get("level", "certified"), rating=fields.get("rating", 5.0), + order_count=fields.get("order_count", 0), + status=fields.get("status", "pending"), + contact=fields.get("contact", ""), created_at=now, updated_at=now) + self.session.add(p) + await self.session.commit() + return self._to_dict(p) + + async def update(self, provider_id: str, fields: dict) -> dict | None: + p = await self.session.get(ServiceProvider, provider_id) + if p is None: + return None + for k in ("name", "category", "level", "status", "contact"): + if k in fields: + setattr(p, k, fields[k]) + if "rating" in fields: + p.rating = fields["rating"] + p.updated_at = utcnow_iso() + await self.session.commit() + return self._to_dict(p) + + +# --------------------------------------------------------------------------- +# 内容 +# --------------------------------------------------------------------------- +class ContentRepository: + def __init__(self, session: AsyncSession): + self.session = session + + def _to_dict(self, c: ContentItem) -> dict: + return { + "id": c.id, "type": c.type, "title": c.title, "summary": c.summary, + "body": c.body, "publisher_id": c.publisher_id, "status": c.status, + "created_at": c.created_at, "updated_at": c.updated_at, + } + + async def list(self, ctype: str | None = None, status: str | None = None) -> list[dict]: + q = select(ContentItem).order_by(ContentItem.created_at.desc()) + if ctype: + q = q.where(ContentItem.type == ctype) + if status: + q = q.where(ContentItem.status == status) + return [self._to_dict(c) for c in await self.session.scalars(q)] + + async def create(self, fields: dict) -> dict: + now = utcnow_iso() + c = ContentItem(id=new_id("cont"), type=fields.get("type", "news"), + title=fields.get("title", ""), summary=fields.get("summary", ""), + body=fields.get("body", ""), publisher_id=fields.get("publisher_id"), + status=fields.get("status", "draft"), created_at=now, updated_at=now) + self.session.add(c) + await self.session.commit() + return self._to_dict(c) + + async def set_status(self, content_id: str, status: str) -> dict | None: + c = await self.session.get(ContentItem, content_id) + if c is None: + return None + c.status = status + c.updated_at = utcnow_iso() + await self.session.commit() + return self._to_dict(c) + + +# --------------------------------------------------------------------------- +# 系统配置 +# --------------------------------------------------------------------------- +class ConfigRepository: + def __init__(self, session: AsyncSession): + self.session = session + + async def all(self) -> list[dict]: + return [ + {"key": c.key, "value": c.value, "description": c.description, + "updated_at": c.updated_at} + for c in await self.session.scalars(select(SystemConfig).order_by(SystemConfig.key)) + ] + + async def get(self, key: str) -> str | None: + c = await self.session.get(SystemConfig, key) + return c.value if c else None + + async def set(self, key: str, value: str, description: str = "") -> dict: + c = await self.session.get(SystemConfig, key) + if c is None: + c = SystemConfig(key=key, value=value, description=description, updated_at=utcnow_iso()) + self.session.add(c) + else: + c.value = value + if description: + c.description = description + c.updated_at = utcnow_iso() + await self.session.commit() + return {"key": c.key, "value": c.value, "description": c.description, "updated_at": c.updated_at} + + +# --------------------------------------------------------------------------- +# OPC 超级个体(工作台聚合数据) +# --------------------------------------------------------------------------- +class OpcProfileRepository: + """OPC 档案(信用评分等)。""" + + def __init__(self, session: AsyncSession): + self.session = session + + async def get(self, user_id: str) -> dict | None: + row = await self.session.get(OpcProfile, user_id) + return self._to_dict(row) if row else None + + async def upsert(self, user_id: str, credit_score: int = 80) -> dict: + row = await self.session.get(OpcProfile, user_id) + if row is None: + row = OpcProfile( + user_id=user_id, credit_score=credit_score, + created_at=utcnow_iso(), updated_at=utcnow_iso(), + ) + self.session.add(row) + else: + row.credit_score = credit_score + row.updated_at = utcnow_iso() + await self.session.commit() + return self._to_dict(row) + + @staticmethod + def _to_dict(p: OpcProfile) -> dict: + return { + "user_id": p.user_id, "credit_score": p.credit_score, + "updated_at": p.updated_at, + } + + +class FinanceRepository: + """OPC 财务流水(收入/支出)与月度聚合。""" + + def __init__(self, session: AsyncSession): + self.session = session + + async def list_by_user(self, user_id: str, category: str | None = None) -> list[dict]: + stmt = select(FinanceRecord).where(FinanceRecord.user_id == user_id) + if category: + stmt = stmt.where(FinanceRecord.category == category) + rows = (await self.session.scalars(stmt.order_by(FinanceRecord.date.desc()))).all() + return [self._to_dict(f) for f in rows] + + async def total_income(self, user_id: str) -> int: + return sum( + f.amount + for f in await self.session.scalars( + select(FinanceRecord).where( + FinanceRecord.user_id == user_id, + FinanceRecord.category == "income", + ) + ) + ) + + async def monthly_income(self, user_id: str, limit: int = 6) -> list[dict]: + """按 YYYY-MM 汇总收入,取最近 N 个月(升序返回)。""" + agg: dict[str, int] = {} + for f in await self.session.scalars( + select(FinanceRecord).where( + FinanceRecord.user_id == user_id, + FinanceRecord.category == "income", + ) + ): + month = (f.date or "")[:7] + if not month: + continue + agg[month] = agg.get(month, 0) + f.amount + months = sorted(agg.keys())[-limit:] + return [{"month": m, "amount": agg[m]} for m in months] + + @staticmethod + def _to_dict(f: FinanceRecord) -> dict: + return { + "id": f.id, "user_id": f.user_id, "category": f.category, + "amount": f.amount, "date": f.date, "note": f.note, + "created_at": f.created_at, + } + + +class MessageRepository: + """OPC 站内消息。""" + + def __init__(self, session: AsyncSession): + self.session = session + + async def recent(self, user_id: str, limit: int = 4) -> list[dict]: + rows = (await self.session.scalars( + select(Message) + .where(Message.user_id == user_id) + .order_by(Message.created_at.desc()) + .limit(limit) + )).all() + return [self._to_dict(m) for m in rows] + + async def list_by_user(self, user_id: str) -> list[dict]: + rows = (await self.session.scalars( + select(Message) + .where(Message.user_id == user_id) + .order_by(Message.created_at.desc()) + )).all() + return [self._to_dict(m) for m in rows] + + async def unread_count(self, user_id: str) -> int: + return len( + (await self.session.scalars( + select(Message).where(Message.user_id == user_id, Message.read.is_(False)) + )).all() + ) + + @staticmethod + def _to_dict(m: Message) -> dict: + return { + "id": m.id, "user_id": m.user_id, "msg_type": m.msg_type, + "title": m.title, "content": m.content, "read": m.read, + "created_at": m.created_at, + } + + +class OpcTaskRepository: + """OPC 个人任务看板。""" + + def __init__(self, session: AsyncSession): + self.session = session + + async def list_by_user(self, user_id: str) -> list[dict]: + rows = (await self.session.scalars( + select(OpcTask) + .where(OpcTask.user_id == user_id) + .order_by(OpcTask.created_at.desc()) + )).all() + return [self._to_dict(t) for t in rows] + + async def counts(self, user_id: str) -> dict: + in_progress = 0 + completed = 0 + for row in await self.session.scalars( + select(OpcTask).where(OpcTask.user_id == user_id) + ): + if row.status == "completed": + completed += 1 + elif row.status in ("in_progress", "urgent"): + in_progress += 1 + return {"in_progress": in_progress, "completed": completed} + + @staticmethod + def _to_dict(t: OpcTask) -> dict: + return { + "id": t.id, "user_id": t.user_id, "title": t.title, "status": t.status, + "budget": t.budget, "deadline": t.deadline, "progress": t.progress, + "created_at": t.created_at, "updated_at": t.updated_at, + } + + +# --------------------------------------------------------------------------- +# 账号↔端口身份(多身份绑定) +# --------------------------------------------------------------------------- +class IdentityRepository: + """一个账号可绑定的多个端口身份(登录后选择其一进入)。""" + + def __init__(self, session: AsyncSession): + self.session = session + + async def _to_dict(self, i: UserIdentity) -> dict: + org = await self.session.get(Organization, i.org_id) if i.org_id else None + region = await self.session.get(Region, i.region_id) if i.region_id else None + return { + "id": i.id, + "user_id": i.user_id, + "port": i.port, + "role": i.role, + "sub_role": i.sub_role, + "name": i.name, + "org_id": i.org_id, + "region_id": i.region_id, + "org_name": org.name if org else None, + "region_name": region.name if region else None, + "status": i.status, + "created_at": i.created_at, + "updated_at": i.updated_at, + } + + async def get(self, identity_id: str) -> dict | None: + row = await self.session.get(UserIdentity, identity_id) + return self._to_dict(row) if row else None + + async def get_for_user(self, identity_id: str, user_id: str) -> dict | None: + row = await self.session.scalar( + select(UserIdentity).where( + UserIdentity.id == identity_id, + UserIdentity.user_id == user_id, + ) + ) + return self._to_dict(row) if row else None + + async def list_for_user(self, user_id: str, active_only: bool = True) -> list[dict]: + stmt = select(UserIdentity).where(UserIdentity.user_id == user_id) + if active_only: + stmt = stmt.where(UserIdentity.status == "active") + rows = (await self.session.scalars(stmt.order_by(UserIdentity.port))).all() + return [self._to_dict(i) for i in rows] + + async def create( + self, + user_id: str, + *, + port: str, + role: str | None = None, + sub_role: str | None = None, + name: str = "", + org_id: str | None = None, + region_id: str | None = None, + ) -> dict: + now = utcnow_iso() + row = UserIdentity( + id=new_id("ident"), + user_id=user_id, + port=port, + role=role or port, + sub_role=sub_role, + name=name, + org_id=org_id, + region_id=region_id, + status="active", + created_at=now, + updated_at=now, + ) + self.session.add(row) + await self.session.commit() + return self._to_dict(row) + + async def set_status(self, identity_id: str, status: str) -> dict | None: + row = await self.session.get(UserIdentity, identity_id) + if row is None: + return None + row.status = status + row.updated_at = utcnow_iso() + await self.session.commit() + return self._to_dict(row) + + +# --------------------------------------------------------------------------- +# 端口工作台(按端口 JSON 载荷) +# --------------------------------------------------------------------------- +class PortalDashboardRepository: + def __init__(self, session: AsyncSession): + self.session = session + + async def get(self, port: str) -> dict | None: + row = await self.session.get(PortalDashboard, port) + if row is None: + return None + try: + payload = json.loads(row.payload) + except (ValueError, TypeError): + return {} + payload["port"] = row.port + payload["updated_at"] = row.updated_at + return payload + + async def set(self, port: str, payload: dict) -> dict: + row = await self.session.get(PortalDashboard, port) + now = utcnow_iso() + data = json.dumps(payload, ensure_ascii=False) + if row is None: + row = PortalDashboard(port=port, payload=data, updated_at=now) + self.session.add(row) + else: + row.payload = data + row.updated_at = now + await self.session.commit() + return await self.get(port) + + +class PortalPageRepository: + """端口子页面数据(按 端口+页面 存 JSON 载荷)。""" + + def __init__(self, session: AsyncSession): + self.session = session + + async def get(self, port: str, page: str) -> dict | None: + row = await self.session.get(PortalPage, (port, page)) + if row is None: + return None + try: + return json.loads(row.payload) + except (ValueError, TypeError): + return {} + + async def set(self, port: str, page: str, payload: dict) -> dict: + row = await self.session.get(PortalPage, (port, page)) + now = utcnow_iso() + data = json.dumps(payload, ensure_ascii=False) + if row is None: + row = PortalPage(port=port, page=page, payload=data, updated_at=now) + self.session.add(row) + else: + row.payload = data + row.updated_at = now + await self.session.commit() + return await self.get(port, page) + + +# --------------------------------------------------------------------------- +# 投资人端(偏好 / 路演 / 报名 / 意向) +# --------------------------------------------------------------------------- +class InvestorPreferenceRepository: + def __init__(self, session: AsyncSession): + self.session = session + + async def get(self, user_id: str) -> dict | None: + row = await self.session.get(InvestorPreference, user_id) + if row is None: + return None + return { + "user_id": row.user_id, + "industries": json.loads(row.industries or "[]"), + "stage": row.stage, + "amount_min": row.amount_min, + "amount_max": row.amount_max, + "region_id": row.region_id, + "updated_at": row.updated_at, + } + + async def upsert(self, user_id: str, fields: dict) -> dict: + row = await self.session.get(InvestorPreference, user_id) + now = utcnow_iso() + if row is None: + row = InvestorPreference(user_id=user_id, updated_at=now) + self.session.add(row) + if "industries" in fields: + row.industries = json.dumps(fields["industries"], ensure_ascii=False) + if "stage" in fields: + row.stage = fields["stage"] or "" + if "amount_min" in fields: + row.amount_min = fields["amount_min"] or 0 + if "amount_max" in fields: + row.amount_max = fields["amount_max"] or 0 + if "region_id" in fields: + row.region_id = fields["region_id"] + row.updated_at = now + await self.session.commit() + return await self.get(user_id) + + +class RoadshowRepository: + def __init__(self, session: AsyncSession): + self.session = session + + async def _to_dict(self, r: Roadshow) -> dict: + region = await self.session.get(Region, r.region_id) if r.region_id else None + return { + "id": r.id, "publisher_id": r.publisher_id, "publisher_role": r.publisher_role, + "title": r.title, "summary": r.summary, "activity_type": r.activity_type, + "scope_type": r.scope_type, "region_id": r.region_id, + "region_name": region.name if region else None, + "start_at": r.start_at, "end_at": r.end_at, "register_deadline": r.register_deadline, + "quota": r.quota, "venue": r.venue, "live_url": r.live_url, + "status": r.status, "need_review": r.need_review, + "review_comment": r.review_comment, "created_at": r.created_at, "updated_at": r.updated_at, + } + + async def list(self, status: str | None = None) -> list[dict]: + stmt = select(Roadshow).order_by(Roadshow.created_at.desc()) + if status: + stmt = stmt.where(Roadshow.status == status) + return [self._to_dict(r) for r in await self.session.scalars(stmt)] + + async def get(self, roadshow_id: str) -> dict | None: + row = await self.session.get(Roadshow, roadshow_id) + return self._to_dict(row) if row else None + + async def create(self, fields: dict) -> dict: + now = utcnow_iso() + row = Roadshow( + id=new_id("rs"), publisher_id=fields.get("publisher_id", ""), + publisher_role=fields.get("publisher_role", "investor"), + title=fields.get("title", ""), summary=fields.get("summary", ""), + activity_type=fields.get("activity_type", "online"), + scope_type=fields.get("scope_type", "all"), + region_id=fields.get("region_id"), + start_at=fields.get("start_at", ""), end_at=fields.get("end_at", ""), + register_deadline=fields.get("register_deadline", ""), + quota=fields.get("quota", 0), venue=fields.get("venue", ""), + live_url=fields.get("live_url", ""), + status=fields.get("status", "draft"), + need_review=fields.get("need_review", True), + created_at=now, updated_at=now, + ) + self.session.add(row) + await self.session.commit() + return self._to_dict(row) + + async def set_status(self, roadshow_id: str, status: str, review_comment: str = "") -> dict | None: + row = await self.session.get(Roadshow, roadshow_id) + if row is None: + return None + row.status = status + if review_comment: + row.review_comment = review_comment + row.updated_at = utcnow_iso() + await self.session.commit() + return self._to_dict(row) + + +class RoadshowRegistrationRepository: + def __init__(self, session: AsyncSession): + self.session = session + + async def create(self, roadshow_id: str, user_id: str, role: str = "investor", note: str = "") -> dict: + row = RoadshowRegistration( + id=new_id("rsreg"), roadshow_id=roadshow_id, user_id=user_id, + role=role, status="applying", note=note, created_at=utcnow_iso(), + ) + self.session.add(row) + await self.session.commit() + return {"id": row.id, "roadshow_id": row.roadshow_id, "user_id": row.user_id, + "role": row.role, "status": row.status, "note": row.note} + + +class InvestmentIntentRepository: + def __init__(self, session: AsyncSession): + self.session = session + + async def create(self, investor_id: str, project_id: str, project_name: str, message: str = "") -> dict: + row = InvestmentIntent( + id=new_id("intent"), investor_id=investor_id, project_id=project_id, + project_name=project_name, status="interested", message=message, + created_at=utcnow_iso(), updated_at=utcnow_iso(), + ) + self.session.add(row) + await self.session.commit() + return {"id": row.id, "investor_id": row.investor_id, "project_id": row.project_id, + "project_name": row.project_name, "status": row.status, "message": row.message} + + async def list_for(self, investor_id: str) -> list[dict]: + rows = (await self.session.scalars( + select(InvestmentIntent).where(InvestmentIntent.investor_id == investor_id) + .order_by(InvestmentIntent.created_at.desc()) + )).all() + return [{"id": r.id, "project_id": r.project_id, "project_name": r.project_name, + "status": r.status, "message": r.message, "created_at": r.created_at} for r in rows] + + +class BidRepository: + """任务竞标。""" + + def __init__(self, session: AsyncSession): + self.session = session + + async def list_for_task(self, task_id: str) -> list[dict]: + rows = (await self.session.scalars( + select(Bid).where(Bid.task_id == task_id).order_by(Bid.created_at.desc()) + )).all() + return [self._to_dict(r) for r in rows] + + async def get(self, bid_id: str) -> dict | None: + row = await self.session.get(Bid, bid_id) + return self._to_dict(row) if row else None + + async def create(self, task_id: str, opc_id: str, opc_name: str, quote: int, plan: str = "") -> dict: + row = Bid( + id=new_id("bid"), task_id=task_id, opc_id=opc_id, opc_name=opc_name, + quote=quote, plan=plan, status="submitted", + created_at=utcnow_iso(), updated_at=utcnow_iso(), + ) + self.session.add(row) + await self.session.commit() + return self._to_dict(row) + + async def set_status(self, bid_id: str, status: str) -> dict | None: + row = await self.session.get(Bid, bid_id) + if row is None: + return None + row.status = status + row.updated_at = utcnow_iso() + await self.session.commit() + return self._to_dict(row) + + @staticmethod + def _to_dict(b: Bid) -> dict: + return {"id": b.id, "task_id": b.task_id, "opc_id": b.opc_id, "opc_name": b.opc_name, + "quote": b.quote, "plan": b.plan, "status": b.status, + "created_at": b.created_at, "updated_at": b.updated_at} + + +class SubsidyRepository: + """补贴申报(政务三级审批)。""" + + def __init__(self, session: AsyncSession): + self.session = session + + async def list(self, region_ids: list[str] | None = None) -> list[dict]: + rows = await self.session.scalars(select(SubsidyApplication).order_by(SubsidyApplication.created_at.desc())) + out = [] + for r in rows: + if region_ids and r.region_id and r.region_id not in region_ids: + continue + out.append(self._to_dict(r)) + return out + + async def get(self, aid: str) -> dict | None: + row = await self.session.get(SubsidyApplication, aid) + return self._to_dict(row) if row else None + + async def create(self, fields: dict) -> dict: + now = utcnow_iso() + row = SubsidyApplication( + id=new_id("sub"), opc_id=fields.get("opc_id", ""), opc_name=fields.get("opc_name", ""), + title=fields.get("title", ""), amount=fields.get("amount", 0), + region_id=fields.get("region_id"), status="applying", + created_at=now, updated_at=now, + ) + self.session.add(row) + await self.session.commit() + return self._to_dict(row) + + async def set_status(self, aid: str, status: str, comment: str = "") -> dict | None: + row = await self.session.get(SubsidyApplication, aid) + if row is None: + return None + row.status = status + if comment: + row.comment = comment + row.updated_at = utcnow_iso() + await self.session.commit() + return self._to_dict(row) + + @staticmethod + def _to_dict(s: SubsidyApplication) -> dict: + return {"id": s.id, "opc_id": s.opc_id, "opc_name": s.opc_name, "title": s.title, + "amount": s.amount, "region_id": s.region_id, "status": s.status, + "comment": s.comment, "created_at": s.created_at} + + +class ServiceReferralRepository: + """载体-服务商引荐。""" + + def __init__(self, session: AsyncSession): + self.session = session + + async def list_for(self, carrier_id: str | None = None) -> list[dict]: + stmt = select(ServiceReferral).order_by(ServiceReferral.created_at.desc()) + if carrier_id: + stmt = stmt.where(ServiceReferral.carrier_id == carrier_id) + return [self._to_dict(r) for r in await self.session.scalars(stmt)] + + async def create(self, fields: dict) -> dict: + row = ServiceReferral( + id=new_id("ref"), carrier_id=fields.get("carrier_id", ""), + provider_id=fields.get("provider_id", ""), provider_name=fields.get("provider_name", ""), + opc_id=fields.get("opc_id", ""), opc_name=fields.get("opc_name", ""), + status="referred", created_at=utcnow_iso(), updated_at=utcnow_iso(), + ) + self.session.add(row) + await self.session.commit() + return self._to_dict(row) + + @staticmethod + def _to_dict(r: ServiceReferral) -> dict: + return {"id": r.id, "carrier_id": r.carrier_id, "provider_id": r.provider_id, + "provider_name": r.provider_name, "opc_id": r.opc_id, "opc_name": r.opc_name, + "status": r.status, "created_at": r.created_at} + + +class TrainingEnrollmentRepository: + """投融资培训报名。""" + + def __init__(self, session: AsyncSession): + self.session = session + + async def list_for(self, user_id: str) -> list[dict]: + rows = (await self.session.scalars( + select(TrainingEnrollment).where(TrainingEnrollment.user_id == user_id) + )).all() + return [{"id": r.id, "training_id": r.training_id, "training_name": r.training_name, + "status": r.status, "created_at": r.created_at} for r in rows] + + async def create(self, user_id: str, training_id: str, training_name: str) -> dict: + row = TrainingEnrollment( + id=new_id("treg"), user_id=user_id, training_id=training_id, + training_name=training_name, status="enrolled", created_at=utcnow_iso(), + ) + self.session.add(row) + await self.session.commit() + return {"id": row.id, "training_id": row.training_id, "training_name": row.training_name, + "status": row.status} + + +class EscrowRepository: + """任务资金托管/结算。""" + + def __init__(self, session: AsyncSession): + self.session = session + + async def list(self, status: str | None = None) -> list[dict]: + stmt = select(Escrow).order_by(Escrow.created_at.desc()) + if status: + stmt = stmt.where(Escrow.status == status) + return [self._to_dict(r) for r in await self.session.scalars(stmt)] + + async def get(self, escrow_id: str) -> dict | None: + row = await self.session.get(Escrow, escrow_id) + return self._to_dict(row) if row else None + + async def create(self, task_id: str, task_title: str, amount: int, commission: int = 0) -> dict: + row = Escrow(id=new_id("esc"), task_id=task_id, task_title=task_title, + amount=amount, commission=commission, status="deposited", + created_at=utcnow_iso(), updated_at=utcnow_iso()) + self.session.add(row) + await self.session.commit() + return self._to_dict(row) + + async def set_status(self, escrow_id: str, status: str) -> dict | None: + row = await self.session.get(Escrow, escrow_id) + if row is None: + return None + row.status = status + row.updated_at = utcnow_iso() + await self.session.commit() + return self._to_dict(row) + + @staticmethod + def _to_dict(e: Escrow) -> dict: + return {"id": e.id, "task_id": e.task_id, "task_title": e.task_title, "amount": e.amount, + "commission": e.commission, "status": e.status, "created_at": e.created_at} + + +class ContractRepository: + def __init__(self, session: AsyncSession): + self.session = session + + async def get_for_task(self, task_id: str) -> dict | None: + row = await self.session.scalar(select(Contract).where(Contract.task_id == task_id)) + return self._to_dict(row) if row else None + + async def create(self, task_id: str, task_title: str, enterprise_id: str, opc_id: str) -> dict: + row = Contract(id=new_id("ct"), task_id=task_id, task_title=task_title, + enterprise_id=enterprise_id, opc_id=opc_id, status="signed", + content=f"任务《{task_title}》电子合同,双方已签署,资金由平台托管。", + created_at=utcnow_iso()) + self.session.add(row) + await self.session.commit() + return self._to_dict(row) + + @staticmethod + def _to_dict(c: Contract) -> dict: + return {"id": c.id, "task_id": c.task_id, "task_title": c.task_title, + "enterprise_id": c.enterprise_id, "opc_id": c.opc_id, "status": c.status, + "content": c.content, "created_at": c.created_at} + + +class DisputeRepository: + def __init__(self, session: AsyncSession): + self.session = session + + async def list(self, status: str | None = None) -> list[dict]: + stmt = select(Dispute).order_by(Dispute.created_at.desc()) + if status: + stmt = stmt.where(Dispute.status == status) + return [self._to_dict(r) for r in await self.session.scalars(stmt)] + + async def create(self, task_id: str, task_title: str, initiator: str, reason: str) -> dict: + row = Dispute(id=new_id("disp"), task_id=task_id, task_title=task_title, + initiator=initiator, reason=reason, status="opened", created_at=utcnow_iso()) + self.session.add(row) + await self.session.commit() + return self._to_dict(row) + + async def set_status(self, dispute_id: str, status: str, resolution: str = "") -> dict | None: + row = await self.session.get(Dispute, dispute_id) + if row is None: + return None + row.status = status + if resolution: + row.resolution = resolution + await self.session.commit() + return self._to_dict(row) + + @staticmethod + def _to_dict(d: Dispute) -> dict: + return {"id": d.id, "task_id": d.task_id, "task_title": d.task_title, + "initiator": d.initiator, "reason": d.reason, "status": d.status, + "resolution": d.resolution, "created_at": d.created_at} + + +class RatingRepository: + def __init__(self, session: AsyncSession): + self.session = session + + async def avg_for(self, user_id: str) -> float: + rows = (await self.session.scalars(select(Rating).where(Rating.to_id == user_id))).all() + return round(sum(r.score for r in rows) / len(rows), 1) if rows else 5.0 + + async def create(self, task_id: str, from_id: str, to_id: str, score: int, comment: str = "") -> dict: + row = Rating(id=new_id("rate"), task_id=task_id, from_id=from_id, to_id=to_id, + score=max(1, min(5, score)), comment=comment, created_at=utcnow_iso()) + self.session.add(row) + await self.session.commit() + return {"id": row.id, "score": row.score, "comment": row.comment} + + +class NotificationRepository: + def __init__(self, session: AsyncSession): + self.session = session + + async def list_for(self, user_id: str, limit: int = 50) -> list[dict]: + rows = (await self.session.scalars( + select(Notification).where(Notification.user_id == user_id) + .order_by(Notification.created_at.desc()).limit(limit) + )).all() + return [{"id": r.id, "type": r.type, "title": r.title, "content": r.content, + "read": r.read, "created_at": r.created_at} for r in rows] + + async def unread(self, user_id: str) -> int: + return len((await self.session.scalars( + select(Notification).where(Notification.user_id == user_id, Notification.read.is_(False)) + )).all()) + + async def create(self, user_id: str, type: str, title: str, content: str) -> dict: + row = Notification(id=new_id("ntf"), user_id=user_id, type=type, title=title, + content=content, read=False, created_at=utcnow_iso()) + self.session.add(row) + await self.session.commit() + return {"id": row.id, "title": row.title} + + async def mark_read(self, user_id: str) -> int: + rows = (await self.session.scalars(select(Notification).where(Notification.user_id == user_id))).all() + for r in rows: + r.read = True + await self.session.commit() + return len(rows) + + +# --------------------------------------------------------------------------- +# 机构成员(机构主账号 + 机构内子账号) +# --------------------------------------------------------------------------- +class OrganizationMemberRepository: + def __init__(self, session: AsyncSession): + self.session = session + + async def list_for_org(self, org_id: str) -> list[dict]: + rows = (await self.session.scalars( + select(OrganizationMember).where(OrganizationMember.org_id == org_id) + )).all() + out = [] + for r in rows: + u = await self.session.get(User, r.user_id) + out.append({"org_id": r.org_id, "user_id": r.user_id, + "username": u.username if u else r.user_id, + "nickname": u.nickname if u else "", + "role": r.role, "is_admin": r.is_admin, "status": r.status}) + return out + + async def is_member(self, org_id: str, user_id: str) -> bool: + return await self.session.get(OrganizationMember, (org_id, user_id)) is not None + + async def is_admin(self, org_id: str, user_id: str) -> bool: + row = await self.session.get(OrganizationMember, (org_id, user_id)) + return bool(row and row.is_admin) + + async def add_member(self, org_id: str, user_id: str, role: str = "member", is_admin: bool = False) -> dict: + row = await self.session.get(OrganizationMember, (org_id, user_id)) + if row is None: + row = OrganizationMember(org_id=org_id, user_id=user_id, role=role, + is_admin=is_admin, status="active", joined_at=utcnow_iso()) + self.session.add(row) + else: + row.role = role + row.is_admin = is_admin + await self.session.commit() + return {"org_id": org_id, "user_id": user_id, "role": role, "is_admin": is_admin} + + +# --------------------------------------------------------------------------- +# 统计 +# --------------------------------------------------------------------------- +class StatsRepository: + """运营端/政务端数据总览(基于现有表聚合)。""" + + def __init__(self, session: AsyncSession): + self.session = session + + async def overview(self, region_ids: list[str] | None = None) -> dict: + users = (await self.session.scalars(select(User))).all() + if region_ids is not None: + scope = set(region_ids) + users = [u for u in users if not u.region_id or u.region_id in scope] + tasks = (await self.session.scalars(select(Task))).all() + providers = (await self.session.scalars(select(ServiceProvider))).all() + content = (await self.session.scalars(select(ContentItem))).all() + active_users = [u for u in users if u.status == "active"] + return { + "user_count": len(users), + "active_user_count": len(active_users), + "task_count": len(tasks), + "published_task_count": len([t for t in tasks if t.status == "published"]), + "provider_count": len(providers), + "active_provider_count": len([p for p in providers if p.status == "active"]), + "content_count": len(content), + "published_content_count": len([c for c in content if c.status == "published"]), + } + + +# --------------------------------------------------------------------------- +# 数据库门面 +# --------------------------------------------------------------------------- +class Database: + """持有全部 Repository,统一访问入口(基础设施层)。""" + + def __init__(self, db_url: str | None = None, session: AsyncSession | None = None): + if session is None: + from sqlalchemy.ext.asyncio import async_sessionmaker + + from .db import make_async_engine + + url = db_url or config.DATABASE_URL + self._engine = make_async_engine(url) + self._session_factory = async_sessionmaker( + bind=self._engine, class_=AsyncSession, autoflush=False, expire_on_commit=False, + ) + self._owns_session = True + else: + self._engine = None + self._session_factory = lambda: session # type: ignore[assignment] + self._owns_session = False + + self.session = self._session_factory() + self.users = UserRepository(self.session) + self.tokens = TokenRepository(self.session) + self.agents = AgentRepository(self.session) + self.roles = RoleRepository(self.session) + self.orgs = OrgRepository(self.session) + self.regions = RegionRepository(self.session) + self.audit = AuditRepository(self.session) + self.tasks = TaskRepository(self.session) + self.providers = ProviderRepository(self.session) + self.content = ContentRepository(self.session) + self.config = ConfigRepository(self.session) + self.identities = IdentityRepository(self.session) + self.portal_dashboards = PortalDashboardRepository(self.session) + self.portal_pages = PortalPageRepository(self.session) + self.opc_profiles = OpcProfileRepository(self.session) + self.finance = FinanceRepository(self.session) + self.messages = MessageRepository(self.session) + self.opc_tasks = OpcTaskRepository(self.session) + self.investor_prefs = InvestorPreferenceRepository(self.session) + self.roadshows = RoadshowRepository(self.session) + self.roadshow_regs = RoadshowRegistrationRepository(self.session) + self.intents = InvestmentIntentRepository(self.session) + self.bids = BidRepository(self.session) + self.subsidies = SubsidyRepository(self.session) + self.referrals = ServiceReferralRepository(self.session) + self.training_enrolls = TrainingEnrollmentRepository(self.session) + self.escrows = EscrowRepository(self.session) + self.contracts = ContractRepository(self.session) + self.disputes = DisputeRepository(self.session) + self.ratings = RatingRepository(self.session) + self.notifications = NotificationRepository(self.session) + self.org_members = OrganizationMemberRepository(self.session) + self.stats = StatsRepository(self.session) + + async def initialize(self) -> None: + """建表(本实例的 engine)+ 幂等种子(roles 空时才写)。""" + from .seed import seed_data + + if self._engine is not None: + from .db import init_db + + await init_db() + await seed_data(self.session) + + async def close(self) -> None: + if self._owns_session: + await self.session.close() diff --git a/app/security.py b/app/infrastructure/security.py similarity index 100% rename from app/security.py rename to app/infrastructure/security.py diff --git a/app/seed.py b/app/infrastructure/seed.py similarity index 95% rename from app/seed.py rename to app/infrastructure/seed.py index fe9eae1..f62e7e9 100644 --- a/app/seed.py +++ b/app/infrastructure/seed.py @@ -9,7 +9,7 @@ from __future__ import annotations import json from sqlalchemy import select -from sqlalchemy.orm import Session +from sqlalchemy.ext.asyncio import AsyncSession from .models import ( AuditLog, @@ -255,10 +255,10 @@ DEMO_USERS = [ ] -def seed_data(session: Session) -> None: +async def seed_data(session: AsyncSession) -> None: """幂等种子:仅当 roles 表为空时写入全部基础数据;身份与 OPC 业务始终补种。""" now = utcnow_iso() - existing = session.scalar(select(Role.id).limit(1)) + existing = await session.scalar(select(Role.id).limit(1)) if existing is not None: # 已初始化的库:仍补种 OPC 业务演示数据、账号身份与端口工作台(各自幂等)。 _sync_permissions(session, now) @@ -272,7 +272,7 @@ def seed_data(session: Session) -> None: _seed_ecosystem(session, now) _ensure_port_agents(session, now) _seed_org_members(session, now) - session.commit() + await session.commit() return session.add_all( @@ -341,14 +341,14 @@ def seed_data(session: Session) -> None: _seed_ecosystem(session, now) _ensure_port_agents(session, now) _seed_org_members(session, now) - session.commit() + await session.commit() # --------------------------------------------------------------------------- # 运营端业务演示数据(任务/服务商/内容/配置) # --------------------------------------------------------------------------- -def _seed_operator_business(session: Session, now: str) -> None: - if session.scalar(select(Task.id).limit(1)) is not None: +async def _seed_operator_business(session: AsyncSession, now: str) -> None: + if await session.scalar(select(Task.id).limit(1)) is not None: return session.add_all([ Task(id="task_001", title="电商小程序首页 UI 设计", category="设计创意", @@ -396,9 +396,9 @@ def _seed_operator_business(session: Session, now: str) -> None: # --------------------------------------------------------------------------- # OPC 超级个体业务演示数据(工作台聚合:档案/任务/财务/消息) # --------------------------------------------------------------------------- -def _seed_opc_business(session: Session, now: str) -> None: +async def _seed_opc_business(session: AsyncSession, now: str) -> None: """OPC 个人工作台演示数据(按用户幂等)。""" - if session.scalar(select(OpcProfile.user_id).limit(1)) is not None: + if await session.scalar(select(OpcProfile.user_id).limit(1)) is not None: return # 档案(信用评分) @@ -491,16 +491,16 @@ def _identity_label(role: str, sub_role: str | None) -> str: return f"{port}-{sub}" if sub else port -def _migrate_identities(session: Session, now: str) -> None: +async def _migrate_identities(session: AsyncSession, now: str) -> None: """为既有账号回填端口身份;并为演示账号补充多身份绑定(幂等)。""" # session 关闭 autoflush:先 flush 让本会话新增的用户可见 - session.flush() + await session.flush() # 1. 每个已有业务角色的用户 -> 一条身份(缺失时补建) - for u in session.scalars(select(User)): + for u in await session.scalars(select(User)): if not u.role: continue - has = session.scalar( + has = await session.scalar( select(UserIdentity.id).where(UserIdentity.user_id == u.id).limit(1) ) if has is not None: @@ -522,10 +522,10 @@ def _migrate_identities(session: Session, now: str) -> None: # 2. 演示多身份绑定:ent01 同时具备「企业-管理员」与「OPC-认证」两个身份, # 用于验证登录后多身份选择流程。 - ent_extra = session.scalar( + ent_extra = await session.scalar( select(UserIdentity.id).where(UserIdentity.id == "ident_ent01_opc").limit(1) ) - if ent_extra is None and session.get(User, "u_ent_01") is not None: + if ent_extra is None and await session.get(User, "u_ent_01") is not None: session.add( UserIdentity( id="ident_ent01_opc", @@ -552,9 +552,9 @@ def _migrate_identities(session: Session, now: str) -> None: ("ident_pine_dev", "developer", "developer", "dev_org_admin", "开放平台-机构开发者"), ] for ident_id, port, role, sub_role, name in _PINE_ALL_PORTS: - if session.scalar(select(UserIdentity.id).where(UserIdentity.id == ident_id).limit(1)) is not None: + if await session.scalar(select(UserIdentity.id).where(UserIdentity.id == ident_id).limit(1)) is not None: continue - if session.get(User, "u_demo_01") is None: + if await session.get(User, "u_demo_01") is None: break session.add( UserIdentity( @@ -646,9 +646,9 @@ _PORT_DASHBOARDS: dict[str, dict] = { } -def _seed_port_dashboards(session: Session, now: str) -> None: +async def _seed_port_dashboards(session: AsyncSession, now: str) -> None: """按端口写入工作台 JSON 载荷(幂等:任一端口已存在则跳过)。""" - if session.scalar(select(PortalDashboard.port).limit(1)) is not None: + if await session.scalar(select(PortalDashboard.port).limit(1)) is not None: return session.add_all( [ @@ -662,9 +662,9 @@ def _seed_port_dashboards(session: Session, now: str) -> None: ) -def _seed_market(session: Session, now: str) -> None: +async def _seed_market(session: AsyncSession, now: str) -> None: """补充平台市场数据(任务广场/政策/服务商),幂等(以 task_mk_001 为标记)。""" - if session.scalar(select(Task.id).where(Task.id == "task_mk_001").limit(1)) is not None: + if await session.scalar(select(Task.id).where(Task.id == "task_mk_001").limit(1)) is not None: return session.add_all([ Task(id="task_mk_001", title="企业官网 UI 设计", category="设计创意", sub_category="UI设计", @@ -873,9 +873,9 @@ _PORT_PAGES: dict[tuple[str, str], dict] = { } -def _seed_port_pages(session: Session, now: str) -> None: +async def _seed_port_pages(session: AsyncSession, now: str) -> None: """写入端口子页面演示数据(幂等:任一 (port,page) 已存在则跳过)。""" - if session.scalar(select(PortalPage.port).limit(1)) is not None: + if await session.scalar(select(PortalPage.port).limit(1)) is not None: return session.add_all( [ @@ -889,9 +889,9 @@ def _seed_port_pages(session: Session, now: str) -> None: ) -def _seed_investor(session: Session, now: str) -> None: +async def _seed_investor(session: AsyncSession, now: str) -> None: """投资人端演示数据(偏好/路演/意向),幂等(以偏好为标记)。""" - if session.scalar(select(InvestorPreference.user_id).limit(1)) is not None: + if await session.scalar(select(InvestorPreference.user_id).limit(1)) is not None: return session.add_all([ InvestorPreference( @@ -934,21 +934,21 @@ def _seed_investor(session: Session, now: str) -> None: ]) -def _sync_permissions(session: Session, now: str) -> None: +async def _sync_permissions(session: AsyncSession, now: str) -> None: """幂等补种权限与角色权限映射(供既有库使用)。 只插入缺失的 Permission 行与缺失的 RolePermission 链接,不删除已有 自定义授权,避免覆盖运营端后续经「角色权限配置」做的调整。 """ for pid, name, category, module in PERMISSIONS: - if session.get(Permission, pid) is None: + if await session.get(Permission, pid) is None: session.add(Permission(id=pid, name=name, category=category, module=module)) for role_id, perms in ROLE_PERMISSIONS.items(): - if session.get(Role, role_id) is None: + if await session.get(Role, role_id) is None: continue existing = { rp.permission_id - for rp in session.scalars( + for rp in await session.scalars( select(RolePermission).where(RolePermission.role_id == role_id) ) } @@ -957,7 +957,7 @@ def _sync_permissions(session: Session, now: str) -> None: session.add(RolePermission(role_id=role_id, permission_id=perm)) -def _ensure_extra_demo_users(session: Session, now: str) -> None: +async def _ensure_extra_demo_users(session: AsyncSession, now: str) -> None: """补建新增的演示用户(投资人等),供既有库使用(幂等)。""" EXTRA = [ ("u_inv_01", "inv01", "投资人账号", "investor", "personal", None, "r_prov_yn"), @@ -977,7 +977,7 @@ def _ensure_extra_demo_users(session: Session, now: str) -> None: ("u_dev_02", "dev02", "机构开发者", "developer", "dev_org_admin", None, "r_prov_yn"), ] for (uid, username, nickname, role, sub_role, org_id, region_id) in EXTRA: - if session.get(User, uid) is not None: + if await session.get(User, uid) is not None: continue digest, salt = hash_password("123456") session.add( @@ -1000,9 +1000,9 @@ def _ensure_extra_demo_users(session: Session, now: str) -> None: ) -def _seed_ecosystem(session: Session, now: str) -> None: +async def _seed_ecosystem(session: AsyncSession, now: str) -> None: """生态横切演示数据(通知/补贴/引荐/培训),幂等。""" - if session.scalar(select(Notification.id).limit(1)) is None: + if await session.scalar(select(Notification.id).limit(1)) is None: session.add_all([ Notification(id="ntf_001", user_id="u_opc_01", type="task", title="任务进度更新", content="您的任务「电商小程序首页 UI 设计」进度已更新", @@ -1012,7 +1012,7 @@ def _seed_ecosystem(session: Session, now: str) -> None: Notification(id="ntf_003", user_id="u_ent_01", type="task", title="收到投标", content="您的任务收到新的竞标", read=False, created_at=now), ]) - if session.scalar(select(SubsidyApplication.id).limit(1)) is not None: + if await session.scalar(select(SubsidyApplication.id).limit(1)) is not None: return session.add_all([ SubsidyApplication(id="sub_001", opc_id="u_opc_01", opc_name="OPC个人创业者", @@ -1036,16 +1036,16 @@ def _seed_ecosystem(session: Session, now: str) -> None: ]) -def _ensure_port_agents(session: Session, now: str) -> None: +async def _ensure_port_agents(session: AsyncSession, now: str) -> None: """为每个账号的每个端口身份确保默认/QA 智能体(多端口彻底隔离)。""" - session.flush() # 确保此前新增的身份可见 - rows = session.execute(select(UserIdentity.user_id, UserIdentity.port)).all() + await session.flush() # 确保此前新增的身份可见 + rows = (await session.execute(select(UserIdentity.user_id, UserIdentity.port))).all() seen: set[tuple[str, str]] = set() for user_id, port in rows: if (user_id, port) in seen: continue seen.add((user_id, port)) - existing = session.scalar( + existing = await session.scalar( select(Agent.id).where(Agent.user_id == user_id, Agent.port == port).limit(1) ) if existing is not None: @@ -1062,10 +1062,10 @@ def _ensure_port_agents(session: Session, now: str) -> None: ) -def _seed_org_members(session: Session, now: str) -> None: +async def _seed_org_members(session: AsyncSession, now: str) -> None: """机构成员(机构主账号 + 子账号),幂等(以 organization_members 为标记)。""" - session.flush() - if session.scalar(select(OrganizationMember.org_id).limit(1)) is not None: + await session.flush() + if await session.scalar(select(OrganizationMember.org_id).limit(1)) is not None: return # 每个机构默认一个管理员主账号 members = [ diff --git a/app/main.py b/app/main.py index 9874cf9..7a03272 100644 --- a/app/main.py +++ b/app/main.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -"""FastAPI 应用入口。 +"""FastAPI 应用入口(接口层组装)。 启动方式(项目根目录): uv run uvicorn app.main:app --host 127.0.0.1 --port 8090 @@ -13,27 +13,27 @@ from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from app import config -from app.repositories import Database -from app.routers import agents as agents_router -from app.routers import auth as auth_router -from app.routers import bootstrap as bootstrap_router -from app.routers import rbac_admin as rbac_admin_router -from app.routers import rbac_government as rbac_government_router -from app.routers import rbac_opc as rbac_opc_router -from app.routers import rbac_investor as rbac_investor_router -from app.routers import rbac_operator as rbac_operator_router -from app.routers import rbac_org as rbac_org_router -from app.routers import rbac_ecosystem as rbac_ecosystem_router -from app.routers import rbac_developer as rbac_developer_router -from app.routers import rbac_portals as rbac_portals_router -from app.routers import templates as templates_router +from app.infrastructure.repositories import Database +from app.api.routers import agents as agents_router +from app.api.routers import auth as auth_router +from app.api.routers import bootstrap as bootstrap_router +from app.api.routers import rbac_admin as rbac_admin_router +from app.api.routers import rbac_government as rbac_government_router +from app.api.routers import rbac_opc as rbac_opc_router +from app.api.routers import rbac_investor as rbac_investor_router +from app.api.routers import rbac_operator as rbac_operator_router +from app.api.routers import rbac_org as rbac_org_router +from app.api.routers import rbac_ecosystem as rbac_ecosystem_router +from app.api.routers import rbac_developer as rbac_developer_router +from app.api.routers import rbac_portals as rbac_portals_router +from app.api.routers import templates as templates_router APP_NAME = "云超服Agents 演示后端" @asynccontextmanager async def lifespan(app: FastAPI): - """启动时初始化 JSON 数据库(首次运行从 data/seed/ 建立数据文件)。 + """启动时初始化数据库(建表 + 幂等种子)。 若 ``app.state.db`` 已由外部注入(如测试夹具),则直接复用, 不重新创建,避免覆盖调用方准备的数据库实例。 @@ -41,17 +41,18 @@ async def lifespan(app: FastAPI): db = getattr(app.state, "db", None) if db is None: db = Database() - db.initialize() + await db.initialize() app.state.db = db yield + if db is not None and db._owns_session: + await db.close() app = FastAPI( title=APP_NAME, description=( - "供 PineAgents 主后端认证转发的演示 FastAPI。" - "数据以本地 JSON 文件存储(见 data/ 目录)," - "下一阶段据此设计数据库。" + "云超服核心服务端:身份/平台 API + 培训子应用(/api/*),统一对外 opc.pinesound.cn。" + "异步四层架构(接口/业务/领域/基础设施)。" ), version="0.1.0", lifespan=lifespan, diff --git a/app/rbac.py b/app/rbac.py index 071581f..e9b1224 100644 --- a/app/rbac.py +++ b/app/rbac.py @@ -10,8 +10,8 @@ from collections.abc import Callable from fastapi import Depends, HTTPException, Request -from .dependencies import get_current_user -from .repositories import Database +from .api.dependencies import get_current_user +from .infrastructure.repositories import Database # 数据范围层级(越靠前权限越大) _SCOPE_ORDER = ("province", "city", "district") @@ -81,7 +81,7 @@ def scope_covers(region_id: str | None) -> Callable: return dep -def write_audit( +async def write_audit( db: Database, *, action: str, @@ -91,9 +91,9 @@ def write_audit( user: dict | None = None, request: Request | None = None, ) -> None: - """写入一条审计日志。""" + """写入一条审计日志(异步)。""" ip = request.client.host if request is not None and request.client else "" - db.audit.add( + await db.audit.add( action=action, resource=resource, resource_id=resource_id, diff --git a/pyproject.toml b/pyproject.toml index 4db44c0..2c42b4a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,11 @@ dependencies = [ "fastapi>=0.115.0", "uvicorn[standard]>=0.30.0", "pydantic>=2.7.0", - "sqlalchemy>=2.0.30", + "sqlalchemy[asyncio]>=2.0.30", + "aiosqlite>=0.20.0", + "asyncmy>=0.2.9", + "redis>=5.0.0", + "aioboto3>=13.0.0", "pyjwt>=2.8.0", "httpx>=0.27.0", "python-multipart>=0.0.12", diff --git a/uv.lock b/uv.lock index a31d6fb..7d0d1a8 100644 --- a/uv.lock +++ b/uv.lock @@ -2,6 +2,209 @@ version = 1 revision = 3 requires-python = ">=3.11" +[[package]] +name = "aioboto3" +version = "15.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiobotocore", extra = ["boto3"] }, + { name = "aiofiles" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a2/01/92e9ab00f36e2899315f49eefcd5b4685fbb19016c7f19a9edf06da80bb0/aioboto3-15.5.0.tar.gz", hash = "sha256:ea8d8787d315594842fbfcf2c4dce3bac2ad61be275bc8584b2ce9a3402a6979", size = 255069, upload-time = "2025-10-30T13:37:16.122Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/3e/e8f5b665bca646d43b916763c901e00a07e40f7746c9128bdc912a089424/aioboto3-15.5.0-py3-none-any.whl", hash = "sha256:cc880c4d6a8481dd7e05da89f41c384dbd841454fc1998ae25ca9c39201437a6", size = 35913, upload-time = "2025-10-30T13:37:14.549Z" }, +] + +[[package]] +name = "aiobotocore" +version = "2.25.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, + { name = "aioitertools" }, + { name = "botocore" }, + { name = "jmespath" }, + { name = "multidict" }, + { name = "python-dateutil" }, + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/62/94/2e4ec48cf1abb89971cb2612d86f979a6240520f0a659b53a43116d344dc/aiobotocore-2.25.1.tar.gz", hash = "sha256:ea9be739bfd7ece8864f072ec99bb9ed5c7e78ebb2b0b15f29781fbe02daedbc", size = 120560, upload-time = "2025-10-28T22:33:21.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/2a/d275ec4ce5cd0096665043995a7d76f5d0524853c76a3d04656de49f8808/aiobotocore-2.25.1-py3-none-any.whl", hash = "sha256:eb6daebe3cbef5b39a0bb2a97cffbe9c7cb46b2fcc399ad141f369f3c2134b1f", size = 86039, upload-time = "2025-10-28T22:33:19.949Z" }, +] + +[package.optional-dependencies] +boto3 = [ + { name = "boto3" }, +] + +[[package]] +name = "aiofiles" +version = "25.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/41/c3/534eac40372d8ee36ef40df62ec129bee4fdb5ad9706e58a29be53b2c970/aiofiles-25.1.0.tar.gz", hash = "sha256:a8d728f0a29de45dc521f18f07297428d56992a742f0cd2701ba86e44d23d5b2", size = 46354, upload-time = "2025-10-09T20:51:04.358Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/8a/340a1555ae33d7354dbca4faa54948d76d89a27ceef032c8c3bc661d003e/aiofiles-25.1.0-py3-none-any.whl", hash = "sha256:abe311e527c862958650f9438e859c1fa7568a141b22abcd015e120e86a85695", size = 14668, upload-time = "2025-10-09T20:51:03.174Z" }, +] + +[[package]] +name = "aiohappyeyeballs" +version = "2.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ce/f4/eec0465c2f67b2664688d0240b3212d5196fd89e741df67ddb81f8d35658/aiohappyeyeballs-2.7.1.tar.gz", hash = "sha256:065665c041c42a5938ed220bdcd7230f22527fbec085e1853d2402c8a3615d9d", size = 24757, upload-time = "2026-07-01T17:11:55.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/43/1947f06babed6b3f1d7f38b0c767f52df66bfb2bc10b468c4a7de9eceff2/aiohappyeyeballs-2.7.1-py3-none-any.whl", hash = "sha256:9243213661e29250eb41368e5daa826fc017156c3b8a11440826b2e3ed376472", size = 15038, upload-time = "2026-07-01T17:11:54.055Z" }, +] + +[[package]] +name = "aiohttp" +version = "3.14.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohappyeyeballs" }, + { name = "aiosignal" }, + { name = "attrs" }, + { name = "frozenlist" }, + { name = "multidict" }, + { name = "propcache" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "yarl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/58/d9/22ce5786ac0c1653ae8b6c23bded02c1686d11f0dbb45b31ce128e0df985/aiohttp-3.14.3.tar.gz", hash = "sha256:9491196535a88924a60afd5b5f434b5b203b6cc616250878dbdb223a8f7844bc", size = 7971213, upload-time = "2026-07-23T01:57:27.037Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/5c/b3e4ff8ad43a8afef9602c5e90285936da1beaea8b029016b793891f03c3/aiohttp-3.14.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e568e14940c09955aa51f4e645b6daa18a581c5dcfcd73744dcc86a856e3ced3", size = 764250, upload-time = "2026-07-23T01:52:48.525Z" }, + { url = "https://files.pythonhosted.org/packages/0e/da/f1b384465e51449d844056b75070461da03a9a23e6c1747003695bf4172a/aiohttp-3.14.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:54cfcdee2770dac994417cbb0ee1f3eb0e7cb6b30c79bf44f2c02ff79ec5124a", size = 516281, upload-time = "2026-07-23T01:52:51.047Z" }, + { url = "https://files.pythonhosted.org/packages/b9/3f/01264f820ee2e3712a827892b1cd6ff80f3300c1fcbffbb45714a915d47a/aiohttp-3.14.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:21c016079415ed3fd676963e9793700a566d85dbbd6bfc564b9b2d209147dcc8", size = 514742, upload-time = "2026-07-23T01:52:53.779Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8d/a71c6f2db52ac1ed142b133f7feddaa6b70539c3f4de24d7e226c95b794c/aiohttp-3.14.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d6088ec9894113802bddb3c09e974929aed2c7b3a8c456219b8aab4481f1a239", size = 1780613, upload-time = "2026-07-23T01:52:56.948Z" }, + { url = "https://files.pythonhosted.org/packages/a5/11/3dd9b3fb3a170f6ec9011b5291d876a6fab4086714c9e158600edf01b4fd/aiohttp-3.14.3-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:16ea7e24c309fb7c0bbd505d149abe4fe4dccfb8db911db7dbec0921bc889a6f", size = 1737688, upload-time = "2026-07-23T01:52:59.294Z" }, + { url = "https://files.pythonhosted.org/packages/6d/3e/834c26918be7d88068822b40e0db30fca50b5f4fe79104aa16a93f1d74e6/aiohttp-3.14.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56f355e79f71aef2a85c80305cc915f894b170dba76de5fe84f6351939b83c06", size = 1845742, upload-time = "2026-07-23T01:53:01.641Z" }, + { url = "https://files.pythonhosted.org/packages/cc/c9/49ab8572df7d66bc13d11e31f781292badb04180dd87ba98733066c6aed7/aiohttp-3.14.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:18c441d0a8fca6de8d1f546849b9f0ab20d435993e2c5b59562b2fae6be2f929", size = 1928412, upload-time = "2026-07-23T01:53:04.018Z" }, + { url = "https://files.pythonhosted.org/packages/a5/b9/2b8f0c0ce09c87a1daf80fd483431b56b1435d3f62789bc86f572e1245de/aiohttp-3.14.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:53e7b4ce82b54a8bcc71b3b67a5cbd177ca1d7f592cbc92cd38b7349f73482db", size = 1786220, upload-time = "2026-07-23T01:53:06.481Z" }, + { url = "https://files.pythonhosted.org/packages/85/00/9c45f81de11710460edfa1dc81317b6e882703b160926c879a9d20da9fcc/aiohttp-3.14.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f55119f7bf25f49ed210f6096090715da24f2943c62102448915fde3c62877ce", size = 1637231, upload-time = "2026-07-23T01:53:10.258Z" }, + { url = "https://files.pythonhosted.org/packages/19/ce/967d628e910756f3539c6107cb7844a1b69440dcb3029a5ee7871b09ab63/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9aa6e61fdf20105c4144e755bd586008ff450791d67b1c8146fdc15959c4d51c", size = 1753161, upload-time = "2026-07-23T01:53:13.817Z" }, + { url = "https://files.pythonhosted.org/packages/11/b2/0c3d4114f0aee4f580f5b3b4eb71b24d7a23b834ea506a4dfebe76513f35/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:ccd4893707b3e2a13e39c90d43cf80edf2e4d0457935bcc103bf2346214c3f15", size = 1756356, upload-time = "2026-07-23T01:53:16.211Z" }, + { url = "https://files.pythonhosted.org/packages/63/5d/99e7d91c82f1399d1ae2a854e080bd1493fbc31e5e959dbc4ec33dac3bec/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b2466434105a4e03113c36ec775cc2ebe6676b62eae326fa670bb607ef788c1c", size = 1819846, upload-time = "2026-07-23T01:53:18.289Z" }, + { url = "https://files.pythonhosted.org/packages/ad/05/d5e1cb6480eeffd3f901d40a2c5e2d1e7effdc797837da3b490272699f13/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:ba59d59aba08ac02fc03b0c8983ccd5ee39a199d0552ce9e6d2b4845b34d59ae", size = 1628531, upload-time = "2026-07-23T01:53:23.86Z" }, + { url = "https://files.pythonhosted.org/packages/c9/90/b934682bcaefae18a9e04f3dff5b68522ba810906358ae5029b68110ea3b/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:ed099d105449c4f9e84f24af203cd131349d4761d8813fa7e02c32e7128cd910", size = 1832712, upload-time = "2026-07-23T01:53:27.551Z" }, + { url = "https://files.pythonhosted.org/packages/21/df/6061679faaf81fac746e7307c7adb71e858071a5d34c27583afefc64f543/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:152516815ef926786a0b6ae2b8f1fd2e0c71582dee0b435636865316fd4891b7", size = 1775014, upload-time = "2026-07-23T01:53:30.223Z" }, + { url = "https://files.pythonhosted.org/packages/8a/1d/f854878bbc69b88faefe924b619a34a6f59ec05fd387c77690667eaa75eb/aiohttp-3.14.3-cp311-cp311-win32.whl", hash = "sha256:a4af35c443e0b1a1bd6a8af3f3485d7fda15c142751a00f3ff8090f0b93346fa", size = 456006, upload-time = "2026-07-23T01:53:34.97Z" }, + { url = "https://files.pythonhosted.org/packages/73/0c/2af9d1674baccd1dbd47282a93d660a22e57ef6167c856deb24b4214fbab/aiohttp-3.14.3-cp311-cp311-win_amd64.whl", hash = "sha256:e1e74298bab6ee0d6e749ed4fd1901c7e604bdda32c03d787a2cc71c46d0433d", size = 481069, upload-time = "2026-07-23T01:53:39.673Z" }, + { url = "https://files.pythonhosted.org/packages/8e/76/88401ff3fc95e85c5fc38d588f36f55e61ecb64343b2bc8d69326f453cc0/aiohttp-3.14.3-cp311-cp311-win_arm64.whl", hash = "sha256:03cd2bde3d7f085b64e549c985f4bb928cad7e8ecf5323bfca320db548d81b39", size = 453021, upload-time = "2026-07-23T01:53:43.749Z" }, + { url = "https://files.pythonhosted.org/packages/18/d4/eb96299230e20acf2efae207cb8d69051f1f68e357e5ea5e479bf6fb097a/aiohttp-3.14.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:39aded8c7f3b935b54aab1d8d73c70ec0ee2d3ec3b943e0e86611bc150ba47f5", size = 754690, upload-time = "2026-07-23T01:53:47.332Z" }, + { url = "https://files.pythonhosted.org/packages/88/11/e7a70a209eb9a067c0d3212b518a0134e3484f5178c7533878b6b514d469/aiohttp-3.14.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5bcb6ff3fdab1258a192679ff1a05d44f59626430aa05cd1a9d2447423599228", size = 509484, upload-time = "2026-07-23T01:53:51.159Z" }, + { url = "https://files.pythonhosted.org/packages/30/07/4bbc222cc8dbe31d4c3e8a5baad2286e4d42026ac0c570027b89afce6344/aiohttp-3.14.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:617105e2c3018ee38d0c8ce5ee3c84f621a6d8b9f723202aacaff28449ca91ee", size = 511949, upload-time = "2026-07-23T01:53:55.083Z" }, + { url = "https://files.pythonhosted.org/packages/54/b9/42e74c46b7b7c794b995bbc1f573fb48950c38b19d8600c62a6804ee2d67/aiohttp-3.14.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f631fe87a6f30df5fbe6d79640b25e4cffb38c31c7fb6f10871517b84b0f8c1a", size = 1765282, upload-time = "2026-07-23T01:53:59.662Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ed/62bc4d74363ad346d518e0720363a949f63e2e23439a79eb5813d4d29bb3/aiohttp-3.14.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a94dbaae5ae27bd849c93570669bff91e0510f33a80805738e3de72a7be0447b", size = 1741511, upload-time = "2026-07-23T01:54:04.063Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9f/181e8a8bc79e47d13c7fc4540bd7a3b729d9505609c61f392a8dd2fbfe55/aiohttp-3.14.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8f2f1c4c032c7cedd7d8da6f54c97b70266c6570c3108d3fdffee7188bb70529", size = 1810680, upload-time = "2026-07-23T01:54:09.882Z" }, + { url = "https://files.pythonhosted.org/packages/5c/9a/dec94d6ad694552fe3424e3f1928d7a606a5d9d9433a04e7ecdd9d38ae7f/aiohttp-3.14.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ea05e1f97ceea523942d9b2a7d7c0359d781d683d6b043f5943a602b14da4787", size = 1905646, upload-time = "2026-07-23T01:54:13.475Z" }, + { url = "https://files.pythonhosted.org/packages/52/b7/7cd31f29d6055bd711ae6e669367fba6f5ae9de463910a793e30556a8db7/aiohttp-3.14.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:543906c127fb1d929b95076db19b83fa2d46751006ff1e23b093aa5ac4d8db42", size = 1792122, upload-time = "2026-07-23T01:54:15.752Z" }, + { url = "https://files.pythonhosted.org/packages/66/73/10b1ef93afa61f4963c746257b70ced619cf31a4798671de5fdb2608501d/aiohttp-3.14.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0a5ff2dfbb9ce645fa5b8ef3e02c6c0b9cc3f6030ff863d0c51fffc50cb5541b", size = 1591127, upload-time = "2026-07-23T01:54:19.489Z" }, + { url = "https://files.pythonhosted.org/packages/49/ed/3b203fa6de1b338c14acdc06bf6ca9b043b7944f005966958c2ced932cde/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:041badb8f84396357c4d3ad26de6afd7a32b112f43d3c63045c0c8278cfd2043", size = 1725210, upload-time = "2026-07-23T01:54:24.129Z" }, + { url = "https://files.pythonhosted.org/packages/28/b7/1c2aab8c706436dcc28598452488ac9cd7c409da815237c28c27d58993e6/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:530125ee1163c4219af35dc3aa1206e541e7b31b6efc1a3f93b70a136f65d427", size = 1764848, upload-time = "2026-07-23T01:54:27.973Z" }, + { url = "https://files.pythonhosted.org/packages/54/50/94c28f08b131c4bf10984ea2c7a536c9920608bb2d6e7f95642c30cc87b7/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c8653fd547c93a61aadc612007790f5555cdd18946fa48cf45e26d8ea4ea473d", size = 1777102, upload-time = "2026-07-23T01:54:31.775Z" }, + { url = "https://files.pythonhosted.org/packages/13/d4/e7d09ba7d345fb2d74440fd2fa033c5e079fac05552927705986f41a364f/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:89176250f686cb9853c0fb7ead90e639e915b84a6f43eedc2a4e7ec21f1037f0", size = 1580205, upload-time = "2026-07-23T01:54:34.518Z" }, + { url = "https://files.pythonhosted.org/packages/a3/84/072a91d68e1e1eb587985b54baab94221277f877e8ef274fc213a0ceae28/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3a26434dafe408229ff3403458ca58de24fb51936504decac49ce6755f77e59d", size = 1797219, upload-time = "2026-07-23T01:54:36.995Z" }, + { url = "https://files.pythonhosted.org/packages/e0/eb/aad34e897e668424d6e995da5dff8a4a09af93363d3392488772957a63aa/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d1558173930a5a8d3069cee5c92fc91c87c4dbcb099debbb3622053717145a19", size = 1768629, upload-time = "2026-07-23T01:54:40.103Z" }, + { url = "https://files.pythonhosted.org/packages/b6/2b/6bb88ddba0fecd9122aa3ebcad25996cf6c083a4a7040dbb3a4f97972af6/aiohttp-3.14.3-cp312-cp312-win32.whl", hash = "sha256:16100ad3ab8d649fdfbee87602d9d2dcdca9df0b9eda8a1b5fdc0d41f96da559", size = 451481, upload-time = "2026-07-23T01:54:42.547Z" }, + { url = "https://files.pythonhosted.org/packages/76/9b/f2f8f108da17ecef2cc3efc424e8b7ad3782b1a8360f7b8eae8ced84f6ea/aiohttp-3.14.3-cp312-cp312-win_amd64.whl", hash = "sha256:33a2d7c28d33797a2e99923dffa63f83d908a19b6bf26cfe80fa790aa5e1a75a", size = 476845, upload-time = "2026-07-23T01:54:44.853Z" }, + { url = "https://files.pythonhosted.org/packages/3e/44/28dac80a8941b604f4da10ce21097614ca1bf905ce93dca28d8d7de9c1e7/aiohttp-3.14.3-cp312-cp312-win_arm64.whl", hash = "sha256:362a3fd481769cac1a824514bcd86fda51c65e8fe6e051099e008fddde6db17c", size = 448050, upload-time = "2026-07-23T01:54:47.087Z" }, + { url = "https://files.pythonhosted.org/packages/57/be/5afd201cc0ab139029aadb75392efe85a293403d9dd3a3226161c21ce00c/aiohttp-3.14.3-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:2e9878ae68e4a5f1c0abe4dd497dbc3d51946f5837b56759e2a02e78fa90ef86", size = 506269, upload-time = "2026-07-23T01:54:49.075Z" }, + { url = "https://files.pythonhosted.org/packages/22/09/dec8189d62b45ade009f6792a2264b942a90cb88aeaf181239933cd72c3c/aiohttp-3.14.3-cp313-cp313-android_21_x86_64.whl", hash = "sha256:f3d2669fe7dec7fc359ecdb5984b29b50d85d5d00f8c1cb61de4f4a24ee42627", size = 515166, upload-time = "2026-07-23T01:54:51.894Z" }, + { url = "https://files.pythonhosted.org/packages/28/24/2854869d29ed8a8b19d74f9ec6629515f7e04d02dd329d9d179201e58e47/aiohttp-3.14.3-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:cc7cb243a68167172f48c1fd43cee91ec4b1d40cefd190edd43369d1a6bc9c82", size = 486263, upload-time = "2026-07-23T01:54:54.223Z" }, + { url = "https://files.pythonhosted.org/packages/d4/dd/57187c8be2a35aea65eaee3bd2c3dcbbcf0204f5106c89637e3610380cd1/aiohttp-3.14.3-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:78253b573e6ffab5028924fc98bc281aae05445969982a10864bc360dea2016c", size = 492299, upload-time = "2026-07-23T01:54:56.236Z" }, + { url = "https://files.pythonhosted.org/packages/b9/11/06ae6ed8f0d414edf4068861e233d8fe23ee699bfd4b3ceb8663db948a62/aiohttp-3.14.3-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7041d52c3a7fa20c9e8c182b534704abb19502c8bdcbde7ab23bfda6f642394f", size = 502235, upload-time = "2026-07-23T01:54:58.377Z" }, + { url = "https://files.pythonhosted.org/packages/7e/a3/559639c34a345d2cf7c52dff6838119f2eaf29eb508227b5b83f573af813/aiohttp-3.14.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ac74facc01463f138b0da5580329cfcc82818dea5656e83ddcd11268fc12ff80", size = 750883, upload-time = "2026-07-23T01:55:00.65Z" }, + { url = "https://files.pythonhosted.org/packages/91/cd/41e131f13afd1e7b0172a9d9eda085ef90eb8439f41f0d279db81ed3ae60/aiohttp-3.14.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d6218d92e450824e9b4881f44e8c09f1853b490f9a64130801024a4793b1b3b0", size = 508473, upload-time = "2026-07-23T01:55:02.945Z" }, + { url = "https://files.pythonhosted.org/packages/bc/6b/e7f13410d391c6e55b4c007a8de024355389d7d459e3d64c42b2d33617e5/aiohttp-3.14.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:11fb37ef075669eee52ab1928fbf6e1741fada40409fa309ebde9607a962aebf", size = 509190, upload-time = "2026-07-23T01:55:05.173Z" }, + { url = "https://files.pythonhosted.org/packages/97/21/6464573e53d69672cc1eada3e5c5cb2d2efa82701e8305a0f2047a576967/aiohttp-3.14.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55bdcc472aafe2de4a253045cc128007a64f1e0264fb675791e132ea5edaa3bd", size = 1761478, upload-time = "2026-07-23T01:55:07.383Z" }, + { url = "https://files.pythonhosted.org/packages/1a/81/d217043a4c17fbce360905e3b2bdd20139ebc9a2de836d035d179c4da006/aiohttp-3.14.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c39846c3aad97a8530c89d7a3869a8f8e9e3762c6ac0504481e5c80948f7e807", size = 1735092, upload-time = "2026-07-23T01:55:09.803Z" }, + { url = "https://files.pythonhosted.org/packages/a1/66/e13a02d0eeb1a9a502402a977abb4e4abff9fe4051c26f80558c57a7c975/aiohttp-3.14.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5895ef58c4620afe02fa16044f023dc4dafec08158f9d08874a46a7dbc0341b8", size = 1800546, upload-time = "2026-07-23T01:55:12.012Z" }, + { url = "https://files.pythonhosted.org/packages/26/5e/57d42fca1d18cb5acc1cad945d017fabc5d6ae71d8a08ad66be8dc3ee544/aiohttp-3.14.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fa9467a8113aa69d3d7c55a70ef0b7c636010a40993f3df9d9d0d73b3eb7ef24", size = 1895250, upload-time = "2026-07-23T01:55:14.357Z" }, + { url = "https://files.pythonhosted.org/packages/ca/1c/7da8d08e74d56f00070822f9638ff3f1c563f8ad87d1efa996c87bfc8644/aiohttp-3.14.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7d2deec16eeedf55f2c7cf75b521ea3856a5177e123844f8fd0f114ce252cb5", size = 1789289, upload-time = "2026-07-23T01:55:16.668Z" }, + { url = "https://files.pythonhosted.org/packages/cd/0f/cf16bcf56896981c1a0319f5d5db9337994b5165730c48a8fa07e9b34be6/aiohttp-3.14.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dd54d0e8717de95939766febac482ac0474d8ac3b048115f9f2b1d23a16e7db4", size = 1586706, upload-time = "2026-07-23T01:55:18.913Z" }, + { url = "https://files.pythonhosted.org/packages/fe/6f/76eac12a7f2480e1e304f842efdb07db33256b0d9165b866b6ef0806c202/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:df82f3787c940c94986b34222d59c9e38843fba85139f36e85255a82ad5355a9", size = 1724652, upload-time = "2026-07-23T01:55:21.296Z" }, + { url = "https://files.pythonhosted.org/packages/39/b6/19c8c592baeeb94b75f966547d40c02ac7590902306ec5863d5c027cf506/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:42a67efc36300d052fb4508a53e8b6901b9284b599ae63945c377569c5fcc1e1", size = 1756239, upload-time = "2026-07-23T01:55:23.705Z" }, + { url = "https://files.pythonhosted.org/packages/dc/c9/4e9383150296f97f873b680c4de8fb2cd88608fb9f48c79edcb111611abc/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7a75aa63cbf9b21cfaf60dc2657e19df2c2867d91707d653fee171ffeedd1371", size = 1769161, upload-time = "2026-07-23T01:55:26.082Z" }, + { url = "https://files.pythonhosted.org/packages/aa/1e/147bdc6cc5de5f3ab011be8bf5d6e786633249f22c20bae06f85e45f5387/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e92eb8acc45eb6a9f4935071a77edf5b85cc6f8dfad5cd99e97653c26593cdde", size = 1578759, upload-time = "2026-07-23T01:55:28.846Z" }, + { url = "https://files.pythonhosted.org/packages/fd/31/78388a9d6040ece2e11df62ea229a822cf5e52d238374b220ae9975b2623/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b014a6ed7cf912e787149fdc529166d3ceabac23f26efeea3158c9aba2354e7e", size = 1792025, upload-time = "2026-07-23T01:55:31.457Z" }, + { url = "https://files.pythonhosted.org/packages/03/51/a3d29fdf2c25d796746af8ad6fe56a45d6256c38b0a8a2ed752e1160b3a2/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3d4f72af88ac2474bb5bca640030320e3d38a0163a1d7533500e87be458eef71", size = 1768477, upload-time = "2026-07-23T01:55:33.87Z" }, + { url = "https://files.pythonhosted.org/packages/29/a6/442e18b5afeade534d877a2dc3c3e392aff8d49787890b0cf84790410267/aiohttp-3.14.3-cp313-cp313-win32.whl", hash = "sha256:5f08ec777f35ee70720233b8b9811d3bb5d728137f30ac91b7457709c3261ac0", size = 451069, upload-time = "2026-07-23T01:55:36.121Z" }, + { url = "https://files.pythonhosted.org/packages/9d/69/3d876ac02659f271cf7f6769f14a8e3de5b6e888ed8b5a7e998086a4cec8/aiohttp-3.14.3-cp313-cp313-win_amd64.whl", hash = "sha256:dff9461ec275f22135650d5ba4b4931a11f3958df7dfbb8db630000d4dee0883", size = 476518, upload-time = "2026-07-23T01:55:38.303Z" }, + { url = "https://files.pythonhosted.org/packages/b2/0e/50d6e6471cd31edce8b282bdec59375a3a69124d8a989a0b1313355cae52/aiohttp-3.14.3-cp313-cp313-win_arm64.whl", hash = "sha256:ddcac3c6b382e81f1dd0499199d4136b877beb4cb5ef770bbbfba56c4b8f55d2", size = 447676, upload-time = "2026-07-23T01:55:40.451Z" }, + { url = "https://files.pythonhosted.org/packages/c8/20/887fdcf832326571b370ffc347b3e70abe101096f3720126aac161b1d872/aiohttp-3.14.3-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:49f7325beb0f85ef4aef5f48f490269575f83e6e2acad00a1d80b807eb027062", size = 509067, upload-time = "2026-07-23T01:55:42.618Z" }, + { url = "https://files.pythonhosted.org/packages/ad/a3/92cec936f78cc4bf0fa5554ebe593b73459d94e3c62303e1902a4cccb6f7/aiohttp-3.14.3-cp314-cp314-android_24_x86_64.whl", hash = "sha256:e3be98a7c30b8c25d573dafba7171d66dfb05ee6a9070fc46535464ff97700a6", size = 514774, upload-time = "2026-07-23T01:55:44.937Z" }, + { url = "https://files.pythonhosted.org/packages/29/ba/2a0c38df3fc557620b6a5acd98364af050053b6285b4dc7ee74100c63c18/aiohttp-3.14.3-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:614c61d478b83953e261d02bb2df750f17227cd33ef8002945bf5aebbde21919", size = 488134, upload-time = "2026-07-23T01:55:47.135Z" }, + { url = "https://files.pythonhosted.org/packages/48/d6/d51b7d4bf309af3693940d8ffd2b9ed0b682434ef85959b7c9c137f60cf8/aiohttp-3.14.3-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:1caa7b0d05f3e3a36f87788c59e970a7ee1cefcfcbb924a9f138c4a6551c9cb7", size = 494201, upload-time = "2026-07-23T01:55:49.451Z" }, + { url = "https://files.pythonhosted.org/packages/3f/5a/8f624384e5f1efabb5229b94157eb966b021e97bdb188c62860c2ae243c2/aiohttp-3.14.3-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:dfa68deb2a443bdaa3ea5297b0699c1464f08aef3812b486d1348eee61b07dc0", size = 502766, upload-time = "2026-07-23T01:55:51.656Z" }, + { url = "https://files.pythonhosted.org/packages/a6/26/4ff0164370deec18fb19254ee4ab10b7a73304ac0c860b13f5f84663759b/aiohttp-3.14.3-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:e72ee89e28d907a18f46959b4eb0bb06701cc7f8cf4366e00029e2ccfaaf5924", size = 756557, upload-time = "2026-07-23T01:55:53.964Z" }, + { url = "https://files.pythonhosted.org/packages/97/a3/7056b86dc0d9ec709ea9777eae3b0161428f943372f8b98c01c11593b682/aiohttp-3.14.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ad4c8b7488d745d2ca4838ebd8ae5ba9b56341d30b1da43640e4ce87f9f49646", size = 510168, upload-time = "2026-07-23T01:55:56.22Z" }, + { url = "https://files.pythonhosted.org/packages/85/ed/0357a015892fd68058bf2d39d3fd1958e459b997a7db30aaa6aaa434ae96/aiohttp-3.14.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:db332af25642007330fca8be5c4d194caf2bea7a7fc84415aff3497af5dfee6b", size = 512957, upload-time = "2026-07-23T01:55:58.437Z" }, + { url = "https://files.pythonhosted.org/packages/47/d1/8aba53f15ccb2238405f5e9d30e2a8ca44f93878c26e7165ade00d374b1c/aiohttp-3.14.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:25bd2708db6bdf6a6630dd37bdcdfcb47c4434d22ac69c64665b802910140b30", size = 1750149, upload-time = "2026-07-23T01:56:00.856Z" }, + { url = "https://files.pythonhosted.org/packages/49/bd/40c3fee327529284375c6701cbb0fa4600cc2e8432af1378f897e2ef7d3a/aiohttp-3.14.3-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:cef89a58e628c4efcac3275c2d68083f82426dcdc89c1492a6f654f9f7ea6ab9", size = 1707685, upload-time = "2026-07-23T01:56:03.371Z" }, + { url = "https://files.pythonhosted.org/packages/2a/a3/ca0cc6724cca8114b05694abd916060758c79894c3aa5b012cdadc1bc28e/aiohttp-3.14.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c23ec8ee9d5ab2f5421f9c7fffce208435607af27fd46d4a44e031954352838f", size = 1803911, upload-time = "2026-07-23T01:56:05.817Z" }, + { url = "https://files.pythonhosted.org/packages/95/b5/85b099c299c3ffd38ad9b3e43694c8a346934e4a30c88c4fd5a841234f77/aiohttp-3.14.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e2667f0bbe7eb6c74eae5e9691441ad186e5845ca3cff63230fc09c4e7514f5d", size = 1876929, upload-time = "2026-07-23T01:56:08.413Z" }, + { url = "https://files.pythonhosted.org/packages/d5/b7/1da684a04175473fa4cddbf9a2f572e79514c3fd27a74597f43057d4f3da/aiohttp-3.14.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18cb43369747b2ae007bd2655fb8e63a099c2ff1d207962943636dac989b3147", size = 1761112, upload-time = "2026-07-23T01:56:10.918Z" }, + { url = "https://files.pythonhosted.org/packages/d1/16/bc4b55e3e5cb175fd69c53c90d60d2f47797cb343da5106e23863dc4dba4/aiohttp-3.14.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d77640cc618c1d99fc4f8589c0f24a730adfa54eb1e57ef7bf0c8dfb78da898c", size = 1583500, upload-time = "2026-07-23T01:56:13.613Z" }, + { url = "https://files.pythonhosted.org/packages/2a/e8/13a9d957a1ee40837f46aa30f0f4c657e673ad86a2e6362a9f9be20d26d9/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:53e5179d8abb5710f8e83ba207c41c8d1261fcffd4616500e15ca2b7a33be10a", size = 1713940, upload-time = "2026-07-23T01:56:15.969Z" }, + { url = "https://files.pythonhosted.org/packages/38/05/d33c680c1bcf1c7e130f9cbfc1fc02fe8bb0c4af2a94a53dd5fb56131e5c/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:cd817772b2fcf2b8c0905795318485f9ec16eae60b29feb7f4c77085311637f0", size = 1724413, upload-time = "2026-07-23T01:56:18.591Z" }, + { url = "https://files.pythonhosted.org/packages/85/1d/af798d306f7a74b6a632dbcabcf62a4c91391b7582d2a8c6d7712e2cc54e/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:4e3ac92d90e92773b2362d506068e9a948192bd553e743c5b2429e28527c8661", size = 1770748, upload-time = "2026-07-23T01:56:21.074Z" }, + { url = "https://files.pythonhosted.org/packages/a8/92/ad720d472556a995049206867765e9410969684f86ee09423ff9969044c1/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:3f42e9b78301f11c8f861746175d8b9c1ccef713fcad9eab396e2f6db8ed4a22", size = 1577564, upload-time = "2026-07-23T01:56:23.475Z" }, + { url = "https://files.pythonhosted.org/packages/60/ad/0ed7586cbef7a884e23a752fa2bb987a122e6a5dd50dab109258d0a95193/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:9d9edccfe496b476db5f398d97b865e9a6752bcf8aec4eef8390ce20fb64bb41", size = 1782080, upload-time = "2026-07-23T01:56:25.994Z" }, + { url = "https://files.pythonhosted.org/packages/97/ea/dbaed0d73e8a69aad653b045dab451c67c2454bb731a37b45a86593e9422/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1c5ec8fb1bcc31a8466f74aaf26c345d5c386fa4bd08a3f0eb9c7a4a3fe8b5bf", size = 1745813, upload-time = "2026-07-23T01:56:28.604Z" }, + { url = "https://files.pythonhosted.org/packages/81/1b/6893d4bc57e434fc93a6c9217c637d967a0b651d989f6e3265179375754a/aiohttp-3.14.3-cp314-cp314-win32.whl", hash = "sha256:38901a84da3ce22249f6e860bf8f90d141bcab7da090cc398f8bb58c0e44b7da", size = 455872, upload-time = "2026-07-23T01:56:31.031Z" }, + { url = "https://files.pythonhosted.org/packages/f5/8b/c7baa1ba1eda4db6989baefe5de6d99834921b84ebd7918624febcb9f290/aiohttp-3.14.3-cp314-cp314-win_amd64.whl", hash = "sha256:8b3b60de05f3dcb6f6a00f818bb2ec781cee4de0645f59ccaf99b1d1823b6100", size = 481030, upload-time = "2026-07-23T01:56:33.365Z" }, + { url = "https://files.pythonhosted.org/packages/22/8c/c29d067df825a2df88ca432db848aa2fe8199598359cc06c12b09320cac9/aiohttp-3.14.3-cp314-cp314-win_arm64.whl", hash = "sha256:1576145bdceeb92382d899751e12743a3a5b8e460a841e3e50543859e54864dc", size = 453669, upload-time = "2026-07-23T01:56:35.731Z" }, + { url = "https://files.pythonhosted.org/packages/6a/a4/9c033beb355d39b6147980597ec9645e4729243f686ee4dc73945de72030/aiohttp-3.14.3-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:8800c996b01c2772a783e3e46f3e1abd5823029adca0df54231960de9bfefa5b", size = 791403, upload-time = "2026-07-23T01:56:37.972Z" }, + { url = "https://files.pythonhosted.org/packages/80/ca/87c32a0a7704583cfc49660bd817889bae5b830bf53b5dcb4e92145ac2da/aiohttp-3.14.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:ebe8e504f058fe91223351cecd2d9d6946c9d241bb0250d898ffbdf584cc72b0", size = 526413, upload-time = "2026-07-23T01:56:40.523Z" }, + { url = "https://files.pythonhosted.org/packages/9e/d8/8ec0e471248c500acdce2be3f46db8fb62b5eb60efef072529cc85ee1d26/aiohttp-3.14.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:30402d03a7c0ff52bce290b57e564e9079fd9d0cb545c8aba73f86a103162d2e", size = 532135, upload-time = "2026-07-23T01:56:42.876Z" }, + { url = "https://files.pythonhosted.org/packages/fe/45/f8919fd936e8b79fcd9bda7b6d8e62613462a713f4f17987fd7c34399142/aiohttp-3.14.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9fc7b5bfec6573f3ae844f457fdde5adeb713f8b8e4a81ad64fc207b49383716", size = 1922742, upload-time = "2026-07-23T01:56:45.528Z" }, + { url = "https://files.pythonhosted.org/packages/f6/ec/9ca76b28a27525b0cc53e20842e0228b022f301ce1f436b7d814b4aaf2df/aiohttp-3.14.3-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8a5fd34f7f7410d1730d5c2ba873cacb2eed3fede366feb268a70ba22581ed8f", size = 1787371, upload-time = "2026-07-23T01:56:48.045Z" }, + { url = "https://files.pythonhosted.org/packages/b1/04/6acdbf17315f7b55f1937e3387acb89a3cddeb4995689553d064af8e92ab/aiohttp-3.14.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:270d3dace9ca2f10f0da5d8ebe519b7a310fc6112ed916e32df5866df0888553", size = 1912623, upload-time = "2026-07-23T01:56:50.605Z" }, + { url = "https://files.pythonhosted.org/packages/86/e6/438b0c79ca6f45eb9fd9817dd4c01a91919a38c0de5ee9e05e2b4dc0ece7/aiohttp-3.14.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3ae5b3a59436d089b5395d910121a390feed4d00578eb95a0fd1a329fe963100", size = 2005515, upload-time = "2026-07-23T01:56:53.153Z" }, + { url = "https://files.pythonhosted.org/packages/bb/6b/62cbd6577758699525f5c712d1ddef57d9875fbab0ae8d5f5a202fd598f8/aiohttp-3.14.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2498f0fe69ead802f9675beca44a7c21c62fdaa4ec5145ea1c3ad6edbee29f85", size = 1879906, upload-time = "2026-07-23T01:56:55.818Z" }, + { url = "https://files.pythonhosted.org/packages/00/95/18bcbf830a21dc3aae24d8f6b6feaf3db1d2090242d00a7868db2ffb0b67/aiohttp-3.14.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a0dc483c00da8b673abbb367eb6f8d8f4bcec30eb58529ea13cb42e7fd2dfa33", size = 1675849, upload-time = "2026-07-23T01:56:58.861Z" }, + { url = "https://files.pythonhosted.org/packages/a9/19/47f4968659c5e23606c3790c80fc624e691c153d036148449ee84d31b287/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c7d3a97c678d34fc5b59da671ee9cd630096ddc643e7b5a30d54a2a6f3574d3f", size = 1843496, upload-time = "2026-07-23T01:57:01.591Z" }, + { url = "https://files.pythonhosted.org/packages/64/af/38c33c4dd82fddcb4e56c4653b6f1072a8edbc6b7fa15809f14932c41e2d/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:f8fb78a83c9e5f741ca3a68cfb455c1f5bb83b4e7249a3848b3cd78d0a8563b0", size = 1827746, upload-time = "2026-07-23T01:57:05.131Z" }, + { url = "https://files.pythonhosted.org/packages/a1/9d/0537cda4885ac8f5b7053d164dd06312f4c483a4edcb8ee5b8aaf2a989bf/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:74ab5b6a9fb13e873e5a90946588baecaf488745e1db1a4a5c433f971f035098", size = 1853810, upload-time = "2026-07-23T01:57:08.043Z" }, + { url = "https://files.pythonhosted.org/packages/19/fe/26f9c5e6458385aa86497836b0dea6fb2f027827d63f37c7856cce9286ee/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:bd52f811e65f6fb634b1047159657c98f52b407f8efec907bcfc09da9a4c0a25", size = 1668895, upload-time = "2026-07-23T01:57:10.837Z" }, + { url = "https://files.pythonhosted.org/packages/ec/4c/618b1db9b9ba079b8875d2cdf78e7c4a3bf72903bd5850fee7dd9544600a/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:f0f177d1b195b9e06376cfd7d308d8a1b920909a609d03ac82a8c73bbb16d3b9", size = 1883833, upload-time = "2026-07-23T01:57:13.672Z" }, + { url = "https://files.pythonhosted.org/packages/94/c6/bd959bd1e4771f9fd944e9e436224c48c77b018b73b519b5aad346335bcc/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:498c6c623134f8e09a3c4e60bcd607a0b4590dd7dbf08dd40851b27cbb520ccb", size = 1844251, upload-time = "2026-07-23T01:57:16.593Z" }, + { url = "https://files.pythonhosted.org/packages/5e/19/08d41839658bdd44a0ed2480f3891705ecb487ce28c0dde62c9040c997e0/aiohttp-3.14.3-cp314-cp314t-win32.whl", hash = "sha256:b304db572b4368edd8dda8a2274f73156fe15558fca4a917cb8a09fc47af5963", size = 474180, upload-time = "2026-07-23T01:57:19.306Z" }, + { url = "https://files.pythonhosted.org/packages/99/5d/3cd6ef0a2b2851f7ab913b5b079334781bd50ff56a323e4454063377a080/aiohttp-3.14.3-cp314-cp314t-win_amd64.whl", hash = "sha256:b20032766aedf6261c7a566585a40867d092ac03a0d81592d5370ef9b054f99b", size = 500528, upload-time = "2026-07-23T01:57:21.762Z" }, + { url = "https://files.pythonhosted.org/packages/a4/37/cfd1ed540a4d318da025590d96b728e63713c09e9377950fc655dadeb856/aiohttp-3.14.3-cp314-cp314t-win_arm64.whl", hash = "sha256:2e1161602f45a54de2ce0905243a95f58cb42dcd378402f3697f5e0b21e9d2e7", size = 469280, upload-time = "2026-07-23T01:57:24.241Z" }, +] + +[[package]] +name = "aioitertools" +version = "0.13.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fd/3c/53c4a17a05fb9ea2313ee1777ff53f5e001aefd5cc85aa2f4c2d982e1e38/aioitertools-0.13.0.tar.gz", hash = "sha256:620bd241acc0bbb9ec819f1ab215866871b4bbd1f73836a55f799200ee86950c", size = 19322, upload-time = "2025-11-06T22:17:07.609Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/a1/510b0a7fadc6f43a6ce50152e69dbd86415240835868bb0bd9b5b88b1e06/aioitertools-0.13.0-py3-none-any.whl", hash = "sha256:0be0292b856f08dfac90e31f4739432f4cb6d7520ab9eb73e143f4f2fa5259be", size = 24182, upload-time = "2025-11-06T22:17:06.502Z" }, +] + +[[package]] +name = "aiosignal" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "frozenlist" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload-time = "2025-07-03T22:54:43.528Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" }, +] + +[[package]] +name = "aiosqlite" +version = "0.22.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/8a/64761f4005f17809769d23e518d915db74e6310474e733e3593cfc854ef1/aiosqlite-0.22.1.tar.gz", hash = "sha256:043e0bd78d32888c0a9ca90fc788b38796843360c855a7262a532813133a0650", size = 14821, upload-time = "2025-12-23T19:25:43.997Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/b7/e3bf5133d697a08128598c8d0abc5e16377b51465a33756de24fa7dee953/aiosqlite-0.22.1-py3-none-any.whl", hash = "sha256:21c002eb13823fad740196c5a2e9d8e62f6243bd9e7e4a1f87fb5e44ecb4fceb", size = 17405, upload-time = "2025-12-23T19:25:42.139Z" }, +] + [[package]] name = "annotated-doc" version = "0.0.5" @@ -33,6 +236,108 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl", hash = "sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494", size = 125813, upload-time = "2026-07-12T20:29:05.763Z" }, ] +[[package]] +name = "async-timeout" +version = "5.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a5/ae/136395dfbfe00dfc94da3f3e136d0b13f394cba8f4841120e34226265780/async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3", size = 9274, upload-time = "2024-11-06T16:41:39.6Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/ba/e2081de779ca30d473f21f5b30e0e737c438205440784c7dfc81efc2b029/async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c", size = 6233, upload-time = "2024-11-06T16:41:37.9Z" }, +] + +[[package]] +name = "asyncmy" +version = "0.2.14" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ca/23/4a77a7776d161e29ff5607d2f33605c9cd4f6be9f9e9cf19c25ee0d5ec1e/asyncmy-0.2.14.tar.gz", hash = "sha256:d058195574cc889f3f773686f7e17d71693641f9ac0386ae59ae623532a841ff", size = 92913, upload-time = "2026-08-12T05:14:06.617Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/f7/f3c304b28fe80b47c42f71e009258fedfb2db46b77cfee760ee2da4adeab/asyncmy-0.2.14-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1e1776aeab4259b832c919f9ecfb1e8fe6df7a05d4ad6a1091928a6539a3e784", size = 2087767, upload-time = "2026-08-12T05:12:15.107Z" }, + { url = "https://files.pythonhosted.org/packages/43/b8/478f4524cecc1b101f5e00688a8575199a9d28a5dada8054cf0acd95af14/asyncmy-0.2.14-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f36c7f94e97a90bfd860a94971eaf31dd4f01f8e4f19991e549535c99dd62287", size = 2050886, upload-time = "2026-08-12T05:12:17.74Z" }, + { url = "https://files.pythonhosted.org/packages/58/64/6cc4d505a040749a25009114c7f1c77ce28676ae563eeaa192dc555b429a/asyncmy-0.2.14-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0cf53a89fc5b8572d90bd549fb427373b04a11b064fbdb619e65d440d6e1109d", size = 6311780, upload-time = "2026-08-12T05:12:19.688Z" }, + { url = "https://files.pythonhosted.org/packages/2c/bb/da4acba71b91c35b8237808ccd73a545e8cae065b3ca3e09a9eda15ec7aa/asyncmy-0.2.14-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9f0c1b334b3dea18d8d7ae461af9ecba91983fa4aea8732c2f78ea854f64ea9f", size = 6350718, upload-time = "2026-08-12T05:12:21.914Z" }, + { url = "https://files.pythonhosted.org/packages/84/f1/cbf4604b4127075d351a11302e3a9e30aa7b18a2f9dce011dcec849c22f5/asyncmy-0.2.14-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:46f6452448762cd1d8699a9f41c1da955c58d6ecbe1eb63e4b34858530294aeb", size = 6137580, upload-time = "2026-08-12T05:12:23.779Z" }, + { url = "https://files.pythonhosted.org/packages/2f/cd/cdff739af31037de179bc930f1fd4069a409bf801ee2b2fe02bda519afdb/asyncmy-0.2.14-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:04e52efa263ece430b3fc17f3dbb0c4b75c7c098a0e4557a5dc4899d28b29409", size = 6250063, upload-time = "2026-08-12T05:12:25.754Z" }, + { url = "https://files.pythonhosted.org/packages/4d/d0/64fabfddc7d261094e7739d9c8ad95a8dc57f4be94735008b5085f8d7d08/asyncmy-0.2.14-cp311-cp311-win32.whl", hash = "sha256:f84c88a33a4a6b02714ce2b1ac2f012db3f6f570fa2746e6b73335541fc33966", size = 1869391, upload-time = "2026-08-12T05:12:27.39Z" }, + { url = "https://files.pythonhosted.org/packages/b3/21/b1547f84dd790400e69f5f93e23a5eaebb9f72ffd6c6e2ff8dc2ab6514ff/asyncmy-0.2.14-cp311-cp311-win_amd64.whl", hash = "sha256:960edb3ffe5c9d44e565f491e4558b49d32b1110281b92ab4a10ba2321df856c", size = 1959747, upload-time = "2026-08-12T05:12:29.022Z" }, + { url = "https://files.pythonhosted.org/packages/15/c8/7b18cf514d2ee509381e6e72fd1818c5d52e53f2eb4b7ce4e3fb59cc11a0/asyncmy-0.2.14-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9c4d3a7982a7a97dcbc9f895f2e9846edd401dd882c5fc24e6587e6b06215f75", size = 2056901, upload-time = "2026-08-12T05:12:31.054Z" }, + { url = "https://files.pythonhosted.org/packages/eb/54/e9f7a0c67c933c406d703d31016d5b0e1c872602ac36d1cfaaf0a2ab5104/asyncmy-0.2.14-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9405dbd5daeed8878a770b9c068201cb52cd0eac1bc1c453e73ad4b5bdb79916", size = 2034676, upload-time = "2026-08-12T05:12:32.445Z" }, + { url = "https://files.pythonhosted.org/packages/1e/21/8f1213ee2567ad7fe5ff51f4bb764ef528ccbaae11a0d57f66f8a4ad1bcd/asyncmy-0.2.14-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:07d9ae2805fdc53cdd38dc4a1e7fb01a35dfcdf6c15664f05c81e7a2703b90b9", size = 6233135, upload-time = "2026-08-12T05:12:34.282Z" }, + { url = "https://files.pythonhosted.org/packages/ba/3e/315a16d189ba13be874cf63d3e7b88f472b5750da908269ab5a22d8d5e7f/asyncmy-0.2.14-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd8e23d2eab3d9249a206f2446e2896ef99893d03ccda900ba5e17018fd44136", size = 6294746, upload-time = "2026-08-12T05:12:36.265Z" }, + { url = "https://files.pythonhosted.org/packages/9b/02/5c6a018b1377a4a5ebd1f6cf3b58e325fd35c1c83f1be50041eb1a45f40b/asyncmy-0.2.14-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:91c4188713db7ab3840e854fe812b1bbffa773073315d0f614884ddcbcc0519a", size = 6024724, upload-time = "2026-08-12T05:12:38.1Z" }, + { url = "https://files.pythonhosted.org/packages/37/95/bb9ea684700d66972767b9a4bef85cc5d0816f2ca45dd48fa86940a5bf88/asyncmy-0.2.14-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a20a8f063d279b96f9ed2d391d7ade82bb3ecda375e21d388c110f979e8cc478", size = 6156140, upload-time = "2026-08-12T05:12:40.47Z" }, + { url = "https://files.pythonhosted.org/packages/2f/dd/90e169494e0998ecd57f55a4b39ee6aeb470d8299503d283903b1286e79a/asyncmy-0.2.14-cp312-cp312-win32.whl", hash = "sha256:63b3f5f052a9b4cbd837a2a867f954f6e437979a7c74a663ac39f911f9da45ce", size = 1846950, upload-time = "2026-08-12T05:12:42.062Z" }, + { url = "https://files.pythonhosted.org/packages/ac/7f/50a56182750805bb08ef7af712138ff5c31a3cf297b4c757b1133f00d259/asyncmy-0.2.14-cp312-cp312-win_amd64.whl", hash = "sha256:999546ded3238150b62d62454f9ee1203368db7673095993f2132b907798ec41", size = 1943255, upload-time = "2026-08-12T05:12:43.647Z" }, + { url = "https://files.pythonhosted.org/packages/19/02/2955b846d1241ed03884e384f8fe301d4cdf65294774acad1b64ecd8985e/asyncmy-0.2.14-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d073fd93612fecfb02218e8a190a0e2f8cac764a24d95752c335244c83b2b9e4", size = 2052132, upload-time = "2026-08-12T05:12:45.273Z" }, + { url = "https://files.pythonhosted.org/packages/17/b2/95698fcaff9a23464f594c506ad1d4a7f7fc5c4d8b6af47c26f604661b68/asyncmy-0.2.14-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e0551a8470b84f9114d359a0bb8e24584d00a42c379a80ae943d338da8b5f5b7", size = 2029223, upload-time = "2026-08-12T05:12:46.724Z" }, + { url = "https://files.pythonhosted.org/packages/56/c7/a375c219f6bffd4bfd3bb1ead40120127b44ca5ef48f40e61b29bb1e755a/asyncmy-0.2.14-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d3b4ed3cb126fa9615c98aa4cfe9f22a75af08ab06253aabedd6c293a9544e9c", size = 6186238, upload-time = "2026-08-12T05:12:48.418Z" }, + { url = "https://files.pythonhosted.org/packages/ac/fa/c501774450db4a18aac1f5d0d47d96fdff54b4836616efd6c8ccf1415e56/asyncmy-0.2.14-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8a9f6cad22be74180bb9ae9569c54bf72e72bf8dad8ec6d5d66e835e6d579651", size = 6236477, upload-time = "2026-08-12T05:12:50.476Z" }, + { url = "https://files.pythonhosted.org/packages/79/75/2e69a287a4d3dcdfd783eb5ac736253ce457d73d39ce21c276b834a1dff2/asyncmy-0.2.14-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4f88d48947ce41ffe4e0488fa80b9d8b745c422a61133e07d250413974e709d3", size = 5971721, upload-time = "2026-08-12T05:12:52.986Z" }, + { url = "https://files.pythonhosted.org/packages/d2/c0/f0a151ee093f8829859b2957934f1654ded7e97d3b8fb8584ed2b6ba246f/asyncmy-0.2.14-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bc0403d1b7625557f966ae16b9798d72d20a6293d6d0989118b8f21261adcb02", size = 6115410, upload-time = "2026-08-12T05:12:54.887Z" }, + { url = "https://files.pythonhosted.org/packages/b2/a9/a62afa69effe2f6407699c0dee22e242469be3fcf2231c0846eb89f9abe9/asyncmy-0.2.14-cp313-cp313-win32.whl", hash = "sha256:969570b5ea070662fc178cd84e58b2d3de791499a5275f2e5f2a2fb31a829666", size = 1844813, upload-time = "2026-08-12T05:12:57.017Z" }, + { url = "https://files.pythonhosted.org/packages/8c/ff/6380c67ea2dd61902ea0704511defd1d15aa68f8965611c4f23e85850588/asyncmy-0.2.14-cp313-cp313-win_amd64.whl", hash = "sha256:fa1d887afa1b5deabad254a864bbfb9e0818810e522cd1037efc1b14823c5007", size = 1940657, upload-time = "2026-08-12T05:12:58.566Z" }, + { url = "https://files.pythonhosted.org/packages/8d/e4/a67bd7df92f587702eaf8d9a031574fe15acc075e14dffa0d400c89619b6/asyncmy-0.2.14-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:23d6cdfbab90c8b0e5da7499b17422de0008d881179650d15d6b3a7fd7f7321e", size = 2807764, upload-time = "2026-08-12T05:13:00.175Z" }, + { url = "https://files.pythonhosted.org/packages/d9/40/c7bcb17220a59709dfd6a2002661276e4d8ad2623b0ffb590a661b7e69b5/asyncmy-0.2.14-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:1c1c56750503b4a98737124e5e6f98a077349f897430c612e7b71994fdfa1fb1", size = 2772816, upload-time = "2026-08-12T05:13:01.635Z" }, + { url = "https://files.pythonhosted.org/packages/07/4b/8565b440e7e580454aa2fc9fa4b8b4db8131e86502af846192d04d5cfde0/asyncmy-0.2.14-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f258d918994ff6c39d01fec17a53de950aa521e70fffe2f9bca8e6c09efe4a06", size = 11243257, upload-time = "2026-08-12T05:13:03.964Z" }, + { url = "https://files.pythonhosted.org/packages/6e/a1/4564fe3f8ef28a86e8cbc5f6a59349155aadf0345641bae6b0013e8f8e78/asyncmy-0.2.14-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00e6b2d37da51e10cd9057df18455d7de6acb03b04bdce1d973b7bbf9efb8356", size = 10962335, upload-time = "2026-08-12T05:13:06.678Z" }, + { url = "https://files.pythonhosted.org/packages/68/4d/131d88e9be4d5d86e5ac038041e2f091f86d9f2eb23ef31efc28fec356bb/asyncmy-0.2.14-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b4c4b52d74e97b323d989d430d115f8d42b6c357ef9f6013790014965b3c2188", size = 10693083, upload-time = "2026-08-12T05:13:09.613Z" }, + { url = "https://files.pythonhosted.org/packages/e7/95/1eeae27be8ac5102d6c7113e757639562d5f46dd33bcbf75d7460a7753ac/asyncmy-0.2.14-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d31e61fac2520319894af6951614e6d82f5c0a0207ab02ae2408ffc69583b669", size = 10700413, upload-time = "2026-08-12T05:13:12.314Z" }, + { url = "https://files.pythonhosted.org/packages/0c/72/65bda5a44d48c540483bcfda961e472fd3d36d41778db77a06ecafa8042d/asyncmy-0.2.14-cp313-cp313t-win32.whl", hash = "sha256:d23f172c101542b5bc93c19dd49ca683eea3211884696c64af89370241e26591", size = 2380351, upload-time = "2026-08-12T05:13:14.361Z" }, + { url = "https://files.pythonhosted.org/packages/eb/02/55dd20ec5910e62757fa48248285bbb124a29e2be9e62b27bce7bcffd39b/asyncmy-0.2.14-cp313-cp313t-win_amd64.whl", hash = "sha256:fb0c5ae02f9e5f360cb645fa02f613c0136ac7488ee4677bb05197546b4a6af1", size = 2568577, upload-time = "2026-08-12T05:13:16.342Z" }, + { url = "https://files.pythonhosted.org/packages/6f/89/733f2cb87224e318c573fd05854ae8b9d21de722dafbc9bf6902e7e131e5/asyncmy-0.2.14-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:74b803d60b00ea476d13912756c2b26f5cc4d70501d1d1c1437f0515bddf8f65", size = 2075378, upload-time = "2026-08-12T05:13:17.742Z" }, + { url = "https://files.pythonhosted.org/packages/cb/81/341f29110611b0f42ad7f266966df26f2975159425d94aa22627e494c5e8/asyncmy-0.2.14-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2d2d98de92eb22d702fd5fa6dc9a39ca20b1de49e5a71437f8e5b13b04a5b7ec", size = 2056634, upload-time = "2026-08-12T05:13:19.265Z" }, + { url = "https://files.pythonhosted.org/packages/bf/6d/904eb0c5a0eb0230b9d06a43e5f3fd26db3f9685d835bfa9e948d4dba26d/asyncmy-0.2.14-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e83bc138424ff4cce01fd5ef9eb624cc98c417105a85375a67e8bbabdb426a55", size = 6207597, upload-time = "2026-08-12T05:13:21.657Z" }, + { url = "https://files.pythonhosted.org/packages/fe/50/abbd49ab1d7e4ce88cf4dc777624d7e5778279acefd78e605dde71da6922/asyncmy-0.2.14-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5d116cd2b5d5715c8cda7c2af1238bf9b36dca210558b0fa309ae9c587369335", size = 6192810, upload-time = "2026-08-12T05:13:23.632Z" }, + { url = "https://files.pythonhosted.org/packages/b5/0d/db165a9d359627c57ba49ad8494bf2a24c4affe96e4bc4322b98fa6179e5/asyncmy-0.2.14-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c46ba9453332bf45e580122ad9bb69657fbb50dbb9684b76ad10d83498372311", size = 6006568, upload-time = "2026-08-12T05:13:25.716Z" }, + { url = "https://files.pythonhosted.org/packages/4a/f7/95910a5c49239b9c186e9eaee899b56c89e8a29ff91c89fc319952c6b33a/asyncmy-0.2.14-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9062b7fdd0e14c32e1fac2114fd72a2d4a4677506a6d1a8a522917ec5c4cf0e0", size = 6087245, upload-time = "2026-08-12T05:13:27.855Z" }, + { url = "https://files.pythonhosted.org/packages/3e/1b/6f990217f27e7490dbf31120e67bd8fcf746e7045ef592fd0ab507e0a7d9/asyncmy-0.2.14-cp314-cp314-win32.whl", hash = "sha256:e4755698751e6f04632abf48ee0d9b8882fb369ae21cb14de6c152d0dc1019c7", size = 1861936, upload-time = "2026-08-12T05:13:29.605Z" }, + { url = "https://files.pythonhosted.org/packages/fa/dd/aff1b47247bde3b638124ffbde4c0f3b1f8728dd0ba24c51bdc3f4b93b1b/asyncmy-0.2.14-cp314-cp314-win_amd64.whl", hash = "sha256:5e5210c013d15c6c01d384d7b1607678f3118121a3ce78c69c4e7ab564c41d8f", size = 1961901, upload-time = "2026-08-12T05:13:31.602Z" }, + { url = "https://files.pythonhosted.org/packages/aa/89/6e6979f8c014ea445facd52168bf4a2871886e4e7c73d856c99ec65f60ce/asyncmy-0.2.14-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5af65cfe97d33efc6697ebc056c714f09eb741555c9fc6d73ce0d9651618484d", size = 2835467, upload-time = "2026-08-12T05:13:33.112Z" }, + { url = "https://files.pythonhosted.org/packages/60/5e/fe7ab0c4398f99e5397566cea513c91db5d739166080c72b445e51a82495/asyncmy-0.2.14-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3f1ea30352fa047f7000cbf56eda605bb8fad5d331bbbf41956d459e00e5e148", size = 2803718, upload-time = "2026-08-12T05:13:34.867Z" }, + { url = "https://files.pythonhosted.org/packages/c4/15/668855756814f6551d023ae07d90a77047eee4f9d5aa55ba8e90ddcb0dc0/asyncmy-0.2.14-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d36816da461bb2d6d5ac910cb07ab83527c0b94cc98753c88b070ebecec77cd0", size = 11269601, upload-time = "2026-08-12T05:13:37.321Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ff/9de0a1ed33dc368d38793ef760003e8cd92b062b9a5c243002f8068752b5/asyncmy-0.2.14-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b0b5b2287c873eda34a449e752eaadc1f23f67e41e74ec01c30e207873ceb8b6", size = 10929177, upload-time = "2026-08-12T05:13:40.73Z" }, + { url = "https://files.pythonhosted.org/packages/e5/24/2359601008327dff0f09ec507db85830469e690d0582e15c2d4f69289821/asyncmy-0.2.14-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:568c3f75403043431a4ccf5f7de5148753d4d3f1d23a2ab76e5bedad270163d1", size = 10731726, upload-time = "2026-08-12T05:13:43.724Z" }, + { url = "https://files.pythonhosted.org/packages/18/4c/ca564c154040c284e8233233660e5f02359b48be20b4b1ce245a71f0d16e/asyncmy-0.2.14-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:62e86e132d4f3b429015c81efd6ed0dfed43cd6820d7c3dd4599cb9bff0d8c44", size = 10684340, upload-time = "2026-08-12T05:13:47.181Z" }, + { url = "https://files.pythonhosted.org/packages/1f/1e/c8b459576b2211f06ce940de89ec188912e13d60e7e9bd18bf92c0143210/asyncmy-0.2.14-cp314-cp314t-win32.whl", hash = "sha256:3ef392a9c7e6d9821a3f265880dab960737aa8e3eccb74a529bca86914a7b712", size = 2413208, upload-time = "2026-08-12T05:13:49.192Z" }, + { url = "https://files.pythonhosted.org/packages/e0/e7/690e3d13935cedb5238556624ea33b25f08cf34e4596cfea0814175c0296/asyncmy-0.2.14-cp314-cp314t-win_amd64.whl", hash = "sha256:b086030b0f647c622c675c090a377fdc6ce94421dc71efa745613df175fd4bff", size = 2609526, upload-time = "2026-08-12T05:13:50.921Z" }, +] + +[[package]] +name = "attrs" +version = "26.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, +] + +[[package]] +name = "boto3" +version = "1.40.61" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore" }, + { name = "jmespath" }, + { name = "s3transfer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ed/f9/6ef8feb52c3cce5ec3967a535a6114b57ac7949fd166b0f3090c2b06e4e5/boto3-1.40.61.tar.gz", hash = "sha256:d6c56277251adf6c2bdd25249feae625abe4966831676689ff23b4694dea5b12", size = 111535, upload-time = "2025-10-28T19:26:57.247Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/24/3bf865b07d15fea85b63504856e137029b6acbc73762496064219cdb265d/boto3-1.40.61-py3-none-any.whl", hash = "sha256:6b9c57b2a922b5d8c17766e29ed792586a818098efe84def27c8f582b33f898c", size = 139321, upload-time = "2025-10-28T19:26:55.007Z" }, +] + +[[package]] +name = "botocore" +version = "1.40.61" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jmespath" }, + { name = "python-dateutil" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/28/a3/81d3a47c2dbfd76f185d3b894f2ad01a75096c006a2dd91f237dca182188/botocore-1.40.61.tar.gz", hash = "sha256:a2487ad69b090f9cccd64cf07c7021cd80ee9c0655ad974f87045b02f3ef52cd", size = 14393956, upload-time = "2025-10-28T19:26:46.108Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/c5/f6ce561004db45f0b847c2cd9b19c67c6bf348a82018a48cb718be6b58b0/botocore-1.40.61-py3-none-any.whl", hash = "sha256:17ebae412692fd4824f99cde0f08d50126dc97954008e5ba2b522eb049238aa7", size = 14055973, upload-time = "2025-10-28T19:26:42.15Z" }, +] + [[package]] name = "certifi" version = "2026.7.22" @@ -79,6 +384,111 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cb/03/10388a42375ee7e4ac9b94eb2c5c569c8b5795e377e701c9ac3ad63de890/fastapi-0.141.1-py3-none-any.whl", hash = "sha256:bfb91aa2d334c61cb35ba9a116fc123b3d3df31640b801cf57a7a78ec3f603b3", size = 131954, upload-time = "2026-07-29T17:18:04.364Z" }, ] +[[package]] +name = "frozenlist" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2d/f5/c831fac6cc817d26fd54c7eaccd04ef7e0288806943f7cc5bbf69f3ac1f0/frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad", size = 45875, upload-time = "2025-10-06T05:38:17.865Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/03/077f869d540370db12165c0aa51640a873fb661d8b315d1d4d67b284d7ac/frozenlist-1.8.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:09474e9831bc2b2199fad6da3c14c7b0fbdd377cce9d3d77131be28906cb7d84", size = 86912, upload-time = "2025-10-06T05:35:45.98Z" }, + { url = "https://files.pythonhosted.org/packages/df/b5/7610b6bd13e4ae77b96ba85abea1c8cb249683217ef09ac9e0ae93f25a91/frozenlist-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:17c883ab0ab67200b5f964d2b9ed6b00971917d5d8a92df149dc2c9779208ee9", size = 50046, upload-time = "2025-10-06T05:35:47.009Z" }, + { url = "https://files.pythonhosted.org/packages/6e/ef/0e8f1fe32f8a53dd26bdd1f9347efe0778b0fddf62789ea683f4cc7d787d/frozenlist-1.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fa47e444b8ba08fffd1c18e8cdb9a75db1b6a27f17507522834ad13ed5922b93", size = 50119, upload-time = "2025-10-06T05:35:48.38Z" }, + { url = "https://files.pythonhosted.org/packages/11/b1/71a477adc7c36e5fb628245dfbdea2166feae310757dea848d02bd0689fd/frozenlist-1.8.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2552f44204b744fba866e573be4c1f9048d6a324dfe14475103fd51613eb1d1f", size = 231067, upload-time = "2025-10-06T05:35:49.97Z" }, + { url = "https://files.pythonhosted.org/packages/45/7e/afe40eca3a2dc19b9904c0f5d7edfe82b5304cb831391edec0ac04af94c2/frozenlist-1.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:957e7c38f250991e48a9a73e6423db1bb9dd14e722a10f6b8bb8e16a0f55f695", size = 233160, upload-time = "2025-10-06T05:35:51.729Z" }, + { url = "https://files.pythonhosted.org/packages/a6/aa/7416eac95603ce428679d273255ffc7c998d4132cfae200103f164b108aa/frozenlist-1.8.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8585e3bb2cdea02fc88ffa245069c36555557ad3609e83be0ec71f54fd4abb52", size = 228544, upload-time = "2025-10-06T05:35:53.246Z" }, + { url = "https://files.pythonhosted.org/packages/8b/3d/2a2d1f683d55ac7e3875e4263d28410063e738384d3adc294f5ff3d7105e/frozenlist-1.8.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:edee74874ce20a373d62dc28b0b18b93f645633c2943fd90ee9d898550770581", size = 243797, upload-time = "2025-10-06T05:35:54.497Z" }, + { url = "https://files.pythonhosted.org/packages/78/1e/2d5565b589e580c296d3bb54da08d206e797d941a83a6fdea42af23be79c/frozenlist-1.8.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c9a63152fe95756b85f31186bddf42e4c02c6321207fd6601a1c89ebac4fe567", size = 247923, upload-time = "2025-10-06T05:35:55.861Z" }, + { url = "https://files.pythonhosted.org/packages/aa/c3/65872fcf1d326a7f101ad4d86285c403c87be7d832b7470b77f6d2ed5ddc/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b6db2185db9be0a04fecf2f241c70b63b1a242e2805be291855078f2b404dd6b", size = 230886, upload-time = "2025-10-06T05:35:57.399Z" }, + { url = "https://files.pythonhosted.org/packages/a0/76/ac9ced601d62f6956f03cc794f9e04c81719509f85255abf96e2510f4265/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f4be2e3d8bc8aabd566f8d5b8ba7ecc09249d74ba3c9ed52e54dc23a293f0b92", size = 245731, upload-time = "2025-10-06T05:35:58.563Z" }, + { url = "https://files.pythonhosted.org/packages/b9/49/ecccb5f2598daf0b4a1415497eba4c33c1e8ce07495eb07d2860c731b8d5/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c8d1634419f39ea6f5c427ea2f90ca85126b54b50837f31497f3bf38266e853d", size = 241544, upload-time = "2025-10-06T05:35:59.719Z" }, + { url = "https://files.pythonhosted.org/packages/53/4b/ddf24113323c0bbcc54cb38c8b8916f1da7165e07b8e24a717b4a12cbf10/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:1a7fa382a4a223773ed64242dbe1c9c326ec09457e6b8428efb4118c685c3dfd", size = 241806, upload-time = "2025-10-06T05:36:00.959Z" }, + { url = "https://files.pythonhosted.org/packages/a7/fb/9b9a084d73c67175484ba2789a59f8eebebd0827d186a8102005ce41e1ba/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:11847b53d722050808926e785df837353bd4d75f1d494377e59b23594d834967", size = 229382, upload-time = "2025-10-06T05:36:02.22Z" }, + { url = "https://files.pythonhosted.org/packages/95/a3/c8fb25aac55bf5e12dae5c5aa6a98f85d436c1dc658f21c3ac73f9fa95e5/frozenlist-1.8.0-cp311-cp311-win32.whl", hash = "sha256:27c6e8077956cf73eadd514be8fb04d77fc946a7fe9f7fe167648b0b9085cc25", size = 39647, upload-time = "2025-10-06T05:36:03.409Z" }, + { url = "https://files.pythonhosted.org/packages/0a/f5/603d0d6a02cfd4c8f2a095a54672b3cf967ad688a60fb9faf04fc4887f65/frozenlist-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:ac913f8403b36a2c8610bbfd25b8013488533e71e62b4b4adce9c86c8cea905b", size = 44064, upload-time = "2025-10-06T05:36:04.368Z" }, + { url = "https://files.pythonhosted.org/packages/5d/16/c2c9ab44e181f043a86f9a8f84d5124b62dbcb3a02c0977ec72b9ac1d3e0/frozenlist-1.8.0-cp311-cp311-win_arm64.whl", hash = "sha256:d4d3214a0f8394edfa3e303136d0575eece0745ff2b47bd2cb2e66dd92d4351a", size = 39937, upload-time = "2025-10-06T05:36:05.669Z" }, + { url = "https://files.pythonhosted.org/packages/69/29/948b9aa87e75820a38650af445d2ef2b6b8a6fab1a23b6bb9e4ef0be2d59/frozenlist-1.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1", size = 87782, upload-time = "2025-10-06T05:36:06.649Z" }, + { url = "https://files.pythonhosted.org/packages/64/80/4f6e318ee2a7c0750ed724fa33a4bdf1eacdc5a39a7a24e818a773cd91af/frozenlist-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b", size = 50594, upload-time = "2025-10-06T05:36:07.69Z" }, + { url = "https://files.pythonhosted.org/packages/2b/94/5c8a2b50a496b11dd519f4a24cb5496cf125681dd99e94c604ccdea9419a/frozenlist-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4", size = 50448, upload-time = "2025-10-06T05:36:08.78Z" }, + { url = "https://files.pythonhosted.org/packages/6a/bd/d91c5e39f490a49df14320f4e8c80161cfcce09f1e2cde1edd16a551abb3/frozenlist-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383", size = 242411, upload-time = "2025-10-06T05:36:09.801Z" }, + { url = "https://files.pythonhosted.org/packages/8f/83/f61505a05109ef3293dfb1ff594d13d64a2324ac3482be2cedc2be818256/frozenlist-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f423a119f4777a4a056b66ce11527366a8bb92f54e541ade21f2374433f6d4", size = 243014, upload-time = "2025-10-06T05:36:11.394Z" }, + { url = "https://files.pythonhosted.org/packages/d8/cb/cb6c7b0f7d4023ddda30cf56b8b17494eb3a79e3fda666bf735f63118b35/frozenlist-1.8.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3462dd9475af2025c31cc61be6652dfa25cbfb56cbbf52f4ccfe029f38decaf8", size = 234909, upload-time = "2025-10-06T05:36:12.598Z" }, + { url = "https://files.pythonhosted.org/packages/31/c5/cd7a1f3b8b34af009fb17d4123c5a778b44ae2804e3ad6b86204255f9ec5/frozenlist-1.8.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4c800524c9cd9bac5166cd6f55285957fcfc907db323e193f2afcd4d9abd69b", size = 250049, upload-time = "2025-10-06T05:36:14.065Z" }, + { url = "https://files.pythonhosted.org/packages/c0/01/2f95d3b416c584a1e7f0e1d6d31998c4a795f7544069ee2e0962a4b60740/frozenlist-1.8.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d6a5df73acd3399d893dafc71663ad22534b5aa4f94e8a2fabfe856c3c1b6a52", size = 256485, upload-time = "2025-10-06T05:36:15.39Z" }, + { url = "https://files.pythonhosted.org/packages/ce/03/024bf7720b3abaebcff6d0793d73c154237b85bdf67b7ed55e5e9596dc9a/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:405e8fe955c2280ce66428b3ca55e12b3c4e9c336fb2103a4937e891c69a4a29", size = 237619, upload-time = "2025-10-06T05:36:16.558Z" }, + { url = "https://files.pythonhosted.org/packages/69/fa/f8abdfe7d76b731f5d8bd217827cf6764d4f1d9763407e42717b4bed50a0/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:908bd3f6439f2fef9e85031b59fd4f1297af54415fb60e4254a95f75b3cab3f3", size = 250320, upload-time = "2025-10-06T05:36:17.821Z" }, + { url = "https://files.pythonhosted.org/packages/f5/3c/b051329f718b463b22613e269ad72138cc256c540f78a6de89452803a47d/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143", size = 246820, upload-time = "2025-10-06T05:36:19.046Z" }, + { url = "https://files.pythonhosted.org/packages/0f/ae/58282e8f98e444b3f4dd42448ff36fa38bef29e40d40f330b22e7108f565/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608", size = 250518, upload-time = "2025-10-06T05:36:20.763Z" }, + { url = "https://files.pythonhosted.org/packages/8f/96/007e5944694d66123183845a106547a15944fbbb7154788cbf7272789536/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa", size = 239096, upload-time = "2025-10-06T05:36:22.129Z" }, + { url = "https://files.pythonhosted.org/packages/66/bb/852b9d6db2fa40be96f29c0d1205c306288f0684df8fd26ca1951d461a56/frozenlist-1.8.0-cp312-cp312-win32.whl", hash = "sha256:433403ae80709741ce34038da08511d4a77062aa924baf411ef73d1146e74faf", size = 39985, upload-time = "2025-10-06T05:36:23.661Z" }, + { url = "https://files.pythonhosted.org/packages/b8/af/38e51a553dd66eb064cdf193841f16f077585d4d28394c2fa6235cb41765/frozenlist-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746", size = 44591, upload-time = "2025-10-06T05:36:24.958Z" }, + { url = "https://files.pythonhosted.org/packages/a7/06/1dc65480ab147339fecc70797e9c2f69d9cea9cf38934ce08df070fdb9cb/frozenlist-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:fe3c58d2f5db5fbd18c2987cba06d51b0529f52bc3a6cdc33d3f4eab725104bd", size = 40102, upload-time = "2025-10-06T05:36:26.333Z" }, + { url = "https://files.pythonhosted.org/packages/2d/40/0832c31a37d60f60ed79e9dfb5a92e1e2af4f40a16a29abcc7992af9edff/frozenlist-1.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a", size = 85717, upload-time = "2025-10-06T05:36:27.341Z" }, + { url = "https://files.pythonhosted.org/packages/30/ba/b0b3de23f40bc55a7057bd38434e25c34fa48e17f20ee273bbde5e0650f3/frozenlist-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96153e77a591c8adc2ee805756c61f59fef4cf4073a9275ee86fe8cba41241f7", size = 49651, upload-time = "2025-10-06T05:36:28.855Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ab/6e5080ee374f875296c4243c381bbdef97a9ac39c6e3ce1d5f7d42cb78d6/frozenlist-1.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40", size = 49417, upload-time = "2025-10-06T05:36:29.877Z" }, + { url = "https://files.pythonhosted.org/packages/d5/4e/e4691508f9477ce67da2015d8c00acd751e6287739123113a9fca6f1604e/frozenlist-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb30f9626572a76dfe4293c7194a09fb1fe93ba94c7d4f720dfae3b646b45027", size = 234391, upload-time = "2025-10-06T05:36:31.301Z" }, + { url = "https://files.pythonhosted.org/packages/40/76/c202df58e3acdf12969a7895fd6f3bc016c642e6726aa63bd3025e0fc71c/frozenlist-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaa352d7047a31d87dafcacbabe89df0aa506abb5b1b85a2fb91bc3faa02d822", size = 233048, upload-time = "2025-10-06T05:36:32.531Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c0/8746afb90f17b73ca5979c7a3958116e105ff796e718575175319b5bb4ce/frozenlist-1.8.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:03ae967b4e297f58f8c774c7eabcce57fe3c2434817d4385c50661845a058121", size = 226549, upload-time = "2025-10-06T05:36:33.706Z" }, + { url = "https://files.pythonhosted.org/packages/7e/eb/4c7eefc718ff72f9b6c4893291abaae5fbc0c82226a32dcd8ef4f7a5dbef/frozenlist-1.8.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6292f1de555ffcc675941d65fffffb0a5bcd992905015f85d0592201793e0e5", size = 239833, upload-time = "2025-10-06T05:36:34.947Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4e/e5c02187cf704224f8b21bee886f3d713ca379535f16893233b9d672ea71/frozenlist-1.8.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29548f9b5b5e3460ce7378144c3010363d8035cea44bc0bf02d57f5a685e084e", size = 245363, upload-time = "2025-10-06T05:36:36.534Z" }, + { url = "https://files.pythonhosted.org/packages/1f/96/cb85ec608464472e82ad37a17f844889c36100eed57bea094518bf270692/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ec3cc8c5d4084591b4237c0a272cc4f50a5b03396a47d9caaf76f5d7b38a4f11", size = 229314, upload-time = "2025-10-06T05:36:38.582Z" }, + { url = "https://files.pythonhosted.org/packages/5d/6f/4ae69c550e4cee66b57887daeebe006fe985917c01d0fff9caab9883f6d0/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:517279f58009d0b1f2e7c1b130b377a349405da3f7621ed6bfae50b10adf20c1", size = 243365, upload-time = "2025-10-06T05:36:40.152Z" }, + { url = "https://files.pythonhosted.org/packages/7a/58/afd56de246cf11780a40a2c28dc7cbabbf06337cc8ddb1c780a2d97e88d8/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:db1e72ede2d0d7ccb213f218df6a078a9c09a7de257c2fe8fcef16d5925230b1", size = 237763, upload-time = "2025-10-06T05:36:41.355Z" }, + { url = "https://files.pythonhosted.org/packages/cb/36/cdfaf6ed42e2644740d4a10452d8e97fa1c062e2a8006e4b09f1b5fd7d63/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b4dec9482a65c54a5044486847b8a66bf10c9cb4926d42927ec4e8fd5db7fed8", size = 240110, upload-time = "2025-10-06T05:36:42.716Z" }, + { url = "https://files.pythonhosted.org/packages/03/a8/9ea226fbefad669f11b52e864c55f0bd57d3c8d7eb07e9f2e9a0b39502e1/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:21900c48ae04d13d416f0e1e0c4d81f7931f73a9dfa0b7a8746fb2fe7dd970ed", size = 233717, upload-time = "2025-10-06T05:36:44.251Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0b/1b5531611e83ba7d13ccc9988967ea1b51186af64c42b7a7af465dcc9568/frozenlist-1.8.0-cp313-cp313-win32.whl", hash = "sha256:8b7b94a067d1c504ee0b16def57ad5738701e4ba10cec90529f13fa03c833496", size = 39628, upload-time = "2025-10-06T05:36:45.423Z" }, + { url = "https://files.pythonhosted.org/packages/d8/cf/174c91dbc9cc49bc7b7aab74d8b734e974d1faa8f191c74af9b7e80848e6/frozenlist-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:878be833caa6a3821caf85eb39c5ba92d28e85df26d57afb06b35b2efd937231", size = 43882, upload-time = "2025-10-06T05:36:46.796Z" }, + { url = "https://files.pythonhosted.org/packages/c1/17/502cd212cbfa96eb1388614fe39a3fc9ab87dbbe042b66f97acb57474834/frozenlist-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:44389d135b3ff43ba8cc89ff7f51f5a0bb6b63d829c8300f79a2fe4fe61bcc62", size = 39676, upload-time = "2025-10-06T05:36:47.8Z" }, + { url = "https://files.pythonhosted.org/packages/d2/5c/3bbfaa920dfab09e76946a5d2833a7cbdf7b9b4a91c714666ac4855b88b4/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e25ac20a2ef37e91c1b39938b591457666a0fa835c7783c3a8f33ea42870db94", size = 89235, upload-time = "2025-10-06T05:36:48.78Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d6/f03961ef72166cec1687e84e8925838442b615bd0b8854b54923ce5b7b8a/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07cdca25a91a4386d2e76ad992916a85038a9b97561bf7a3fd12d5d9ce31870c", size = 50742, upload-time = "2025-10-06T05:36:49.837Z" }, + { url = "https://files.pythonhosted.org/packages/1e/bb/a6d12b7ba4c3337667d0e421f7181c82dda448ce4e7ad7ecd249a16fa806/frozenlist-1.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4e0c11f2cc6717e0a741f84a527c52616140741cd812a50422f83dc31749fb52", size = 51725, upload-time = "2025-10-06T05:36:50.851Z" }, + { url = "https://files.pythonhosted.org/packages/bc/71/d1fed0ffe2c2ccd70b43714c6cab0f4188f09f8a67a7914a6b46ee30f274/frozenlist-1.8.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b3210649ee28062ea6099cfda39e147fa1bc039583c8ee4481cb7811e2448c51", size = 284533, upload-time = "2025-10-06T05:36:51.898Z" }, + { url = "https://files.pythonhosted.org/packages/c9/1f/fb1685a7b009d89f9bf78a42d94461bc06581f6e718c39344754a5d9bada/frozenlist-1.8.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581ef5194c48035a7de2aefc72ac6539823bb71508189e5de01d60c9dcd5fa65", size = 292506, upload-time = "2025-10-06T05:36:53.101Z" }, + { url = "https://files.pythonhosted.org/packages/e6/3b/b991fe1612703f7e0d05c0cf734c1b77aaf7c7d321df4572e8d36e7048c8/frozenlist-1.8.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3ef2d026f16a2b1866e1d86fc4e1291e1ed8a387b2c333809419a2f8b3a77b82", size = 274161, upload-time = "2025-10-06T05:36:54.309Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ec/c5c618767bcdf66e88945ec0157d7f6c4a1322f1473392319b7a2501ded7/frozenlist-1.8.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5500ef82073f599ac84d888e3a8c1f77ac831183244bfd7f11eaa0289fb30714", size = 294676, upload-time = "2025-10-06T05:36:55.566Z" }, + { url = "https://files.pythonhosted.org/packages/7c/ce/3934758637d8f8a88d11f0585d6495ef54b2044ed6ec84492a91fa3b27aa/frozenlist-1.8.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50066c3997d0091c411a66e710f4e11752251e6d2d73d70d8d5d4c76442a199d", size = 300638, upload-time = "2025-10-06T05:36:56.758Z" }, + { url = "https://files.pythonhosted.org/packages/fc/4f/a7e4d0d467298f42de4b41cbc7ddaf19d3cfeabaf9ff97c20c6c7ee409f9/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5c1c8e78426e59b3f8005e9b19f6ff46e5845895adbde20ece9218319eca6506", size = 283067, upload-time = "2025-10-06T05:36:57.965Z" }, + { url = "https://files.pythonhosted.org/packages/dc/48/c7b163063d55a83772b268e6d1affb960771b0e203b632cfe09522d67ea5/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:eefdba20de0d938cec6a89bd4d70f346a03108a19b9df4248d3cf0d88f1b0f51", size = 292101, upload-time = "2025-10-06T05:36:59.237Z" }, + { url = "https://files.pythonhosted.org/packages/9f/d0/2366d3c4ecdc2fd391e0afa6e11500bfba0ea772764d631bbf82f0136c9d/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cf253e0e1c3ceb4aaff6df637ce033ff6535fb8c70a764a8f46aafd3d6ab798e", size = 289901, upload-time = "2025-10-06T05:37:00.811Z" }, + { url = "https://files.pythonhosted.org/packages/b8/94/daff920e82c1b70e3618a2ac39fbc01ae3e2ff6124e80739ce5d71c9b920/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0", size = 289395, upload-time = "2025-10-06T05:37:02.115Z" }, + { url = "https://files.pythonhosted.org/packages/e3/20/bba307ab4235a09fdcd3cc5508dbabd17c4634a1af4b96e0f69bfe551ebd/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6da155091429aeba16851ecb10a9104a108bcd32f6c1642867eadaee401c1c41", size = 283659, upload-time = "2025-10-06T05:37:03.711Z" }, + { url = "https://files.pythonhosted.org/packages/fd/00/04ca1c3a7a124b6de4f8a9a17cc2fcad138b4608e7a3fc5877804b8715d7/frozenlist-1.8.0-cp313-cp313t-win32.whl", hash = "sha256:0f96534f8bfebc1a394209427d0f8a63d343c9779cda6fc25e8e121b5fd8555b", size = 43492, upload-time = "2025-10-06T05:37:04.915Z" }, + { url = "https://files.pythonhosted.org/packages/59/5e/c69f733a86a94ab10f68e496dc6b7e8bc078ebb415281d5698313e3af3a1/frozenlist-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5d63a068f978fc69421fb0e6eb91a9603187527c86b7cd3f534a5b77a592b888", size = 48034, upload-time = "2025-10-06T05:37:06.343Z" }, + { url = "https://files.pythonhosted.org/packages/16/6c/be9d79775d8abe79b05fa6d23da99ad6e7763a1d080fbae7290b286093fd/frozenlist-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf0a7e10b077bf5fb9380ad3ae8ce20ef919a6ad93b4552896419ac7e1d8e042", size = 41749, upload-time = "2025-10-06T05:37:07.431Z" }, + { url = "https://files.pythonhosted.org/packages/f1/c8/85da824b7e7b9b6e7f7705b2ecaf9591ba6f79c1177f324c2735e41d36a2/frozenlist-1.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cee686f1f4cadeb2136007ddedd0aaf928ab95216e7691c63e50a8ec066336d0", size = 86127, upload-time = "2025-10-06T05:37:08.438Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e8/a1185e236ec66c20afd72399522f142c3724c785789255202d27ae992818/frozenlist-1.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:119fb2a1bd47307e899c2fac7f28e85b9a543864df47aa7ec9d3c1b4545f096f", size = 49698, upload-time = "2025-10-06T05:37:09.48Z" }, + { url = "https://files.pythonhosted.org/packages/a1/93/72b1736d68f03fda5fdf0f2180fb6caaae3894f1b854d006ac61ecc727ee/frozenlist-1.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c", size = 49749, upload-time = "2025-10-06T05:37:10.569Z" }, + { url = "https://files.pythonhosted.org/packages/a7/b2/fabede9fafd976b991e9f1b9c8c873ed86f202889b864756f240ce6dd855/frozenlist-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:cba69cb73723c3f329622e34bdbf5ce1f80c21c290ff04256cff1cd3c2036ed2", size = 231298, upload-time = "2025-10-06T05:37:11.993Z" }, + { url = "https://files.pythonhosted.org/packages/3a/3b/d9b1e0b0eed36e70477ffb8360c49c85c8ca8ef9700a4e6711f39a6e8b45/frozenlist-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:778a11b15673f6f1df23d9586f83c4846c471a8af693a22e066508b77d201ec8", size = 232015, upload-time = "2025-10-06T05:37:13.194Z" }, + { url = "https://files.pythonhosted.org/packages/dc/94/be719d2766c1138148564a3960fc2c06eb688da592bdc25adcf856101be7/frozenlist-1.8.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686", size = 225038, upload-time = "2025-10-06T05:37:14.577Z" }, + { url = "https://files.pythonhosted.org/packages/e4/09/6712b6c5465f083f52f50cf74167b92d4ea2f50e46a9eea0523d658454ae/frozenlist-1.8.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:97260ff46b207a82a7567b581ab4190bd4dfa09f4db8a8b49d1a958f6aa4940e", size = 240130, upload-time = "2025-10-06T05:37:15.781Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d4/cd065cdcf21550b54f3ce6a22e143ac9e4836ca42a0de1022da8498eac89/frozenlist-1.8.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54b2077180eb7f83dd52c40b2750d0a9f175e06a42e3213ce047219de902717a", size = 242845, upload-time = "2025-10-06T05:37:17.037Z" }, + { url = "https://files.pythonhosted.org/packages/62/c3/f57a5c8c70cd1ead3d5d5f776f89d33110b1addae0ab010ad774d9a44fb9/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2f05983daecab868a31e1da44462873306d3cbfd76d1f0b5b69c473d21dbb128", size = 229131, upload-time = "2025-10-06T05:37:18.221Z" }, + { url = "https://files.pythonhosted.org/packages/6c/52/232476fe9cb64f0742f3fde2b7d26c1dac18b6d62071c74d4ded55e0ef94/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:33f48f51a446114bc5d251fb2954ab0164d5be02ad3382abcbfe07e2531d650f", size = 240542, upload-time = "2025-10-06T05:37:19.771Z" }, + { url = "https://files.pythonhosted.org/packages/5f/85/07bf3f5d0fb5414aee5f47d33c6f5c77bfe49aac680bfece33d4fdf6a246/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:154e55ec0655291b5dd1b8731c637ecdb50975a2ae70c606d100750a540082f7", size = 237308, upload-time = "2025-10-06T05:37:20.969Z" }, + { url = "https://files.pythonhosted.org/packages/11/99/ae3a33d5befd41ac0ca2cc7fd3aa707c9c324de2e89db0e0f45db9a64c26/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4314debad13beb564b708b4a496020e5306c7333fa9a3ab90374169a20ffab30", size = 238210, upload-time = "2025-10-06T05:37:22.252Z" }, + { url = "https://files.pythonhosted.org/packages/b2/60/b1d2da22f4970e7a155f0adde9b1435712ece01b3cd45ba63702aea33938/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:073f8bf8becba60aa931eb3bc420b217bb7d5b8f4750e6f8b3be7f3da85d38b7", size = 231972, upload-time = "2025-10-06T05:37:23.5Z" }, + { url = "https://files.pythonhosted.org/packages/3f/ab/945b2f32de889993b9c9133216c068b7fcf257d8595a0ac420ac8677cab0/frozenlist-1.8.0-cp314-cp314-win32.whl", hash = "sha256:bac9c42ba2ac65ddc115d930c78d24ab8d4f465fd3fc473cdedfccadb9429806", size = 40536, upload-time = "2025-10-06T05:37:25.581Z" }, + { url = "https://files.pythonhosted.org/packages/59/ad/9caa9b9c836d9ad6f067157a531ac48b7d36499f5036d4141ce78c230b1b/frozenlist-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0", size = 44330, upload-time = "2025-10-06T05:37:26.928Z" }, + { url = "https://files.pythonhosted.org/packages/82/13/e6950121764f2676f43534c555249f57030150260aee9dcf7d64efda11dd/frozenlist-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:d1eaff1d00c7751b7c6662e9c5ba6eb2c17a2306ba5e2a37f24ddf3cc953402b", size = 40627, upload-time = "2025-10-06T05:37:28.075Z" }, + { url = "https://files.pythonhosted.org/packages/c0/c7/43200656ecc4e02d3f8bc248df68256cd9572b3f0017f0a0c4e93440ae23/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d3bb933317c52d7ea5004a1c442eef86f426886fba134ef8cf4226ea6ee1821d", size = 89238, upload-time = "2025-10-06T05:37:29.373Z" }, + { url = "https://files.pythonhosted.org/packages/d1/29/55c5f0689b9c0fb765055629f472c0de484dcaf0acee2f7707266ae3583c/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8009897cdef112072f93a0efdce29cd819e717fd2f649ee3016efd3cd885a7ed", size = 50738, upload-time = "2025-10-06T05:37:30.792Z" }, + { url = "https://files.pythonhosted.org/packages/ba/7d/b7282a445956506fa11da8c2db7d276adcbf2b17d8bb8407a47685263f90/frozenlist-1.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2c5dcbbc55383e5883246d11fd179782a9d07a986c40f49abe89ddf865913930", size = 51739, upload-time = "2025-10-06T05:37:32.127Z" }, + { url = "https://files.pythonhosted.org/packages/62/1c/3d8622e60d0b767a5510d1d3cf21065b9db874696a51ea6d7a43180a259c/frozenlist-1.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:39ecbc32f1390387d2aa4f5a995e465e9e2f79ba3adcac92d68e3e0afae6657c", size = 284186, upload-time = "2025-10-06T05:37:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/2d/14/aa36d5f85a89679a85a1d44cd7a6657e0b1c75f61e7cad987b203d2daca8/frozenlist-1.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92db2bf818d5cc8d9c1f1fc56b897662e24ea5adb36ad1f1d82875bd64e03c24", size = 292196, upload-time = "2025-10-06T05:37:36.107Z" }, + { url = "https://files.pythonhosted.org/packages/05/23/6bde59eb55abd407d34f77d39a5126fb7b4f109a3f611d3929f14b700c66/frozenlist-1.8.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dc43a022e555de94c3b68a4ef0b11c4f747d12c024a520c7101709a2144fb37", size = 273830, upload-time = "2025-10-06T05:37:37.663Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3f/22cff331bfad7a8afa616289000ba793347fcd7bc275f3b28ecea2a27909/frozenlist-1.8.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb89a7f2de3602cfed448095bab3f178399646ab7c61454315089787df07733a", size = 294289, upload-time = "2025-10-06T05:37:39.261Z" }, + { url = "https://files.pythonhosted.org/packages/a4/89/5b057c799de4838b6c69aa82b79705f2027615e01be996d2486a69ca99c4/frozenlist-1.8.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:33139dc858c580ea50e7e60a1b0ea003efa1fd42e6ec7fdbad78fff65fad2fd2", size = 300318, upload-time = "2025-10-06T05:37:43.213Z" }, + { url = "https://files.pythonhosted.org/packages/30/de/2c22ab3eb2a8af6d69dc799e48455813bab3690c760de58e1bf43b36da3e/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:168c0969a329b416119507ba30b9ea13688fafffac1b7822802537569a1cb0ef", size = 282814, upload-time = "2025-10-06T05:37:45.337Z" }, + { url = "https://files.pythonhosted.org/packages/59/f7/970141a6a8dbd7f556d94977858cfb36fa9b66e0892c6dd780d2219d8cd8/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:28bd570e8e189d7f7b001966435f9dac6718324b5be2990ac496cf1ea9ddb7fe", size = 291762, upload-time = "2025-10-06T05:37:46.657Z" }, + { url = "https://files.pythonhosted.org/packages/c1/15/ca1adae83a719f82df9116d66f5bb28bb95557b3951903d39135620ef157/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b2a095d45c5d46e5e79ba1e5b9cb787f541a8dee0433836cea4b96a2c439dcd8", size = 289470, upload-time = "2025-10-06T05:37:47.946Z" }, + { url = "https://files.pythonhosted.org/packages/ac/83/dca6dc53bf657d371fbc88ddeb21b79891e747189c5de990b9dfff2ccba1/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:eab8145831a0d56ec9c4139b6c3e594c7a83c2c8be25d5bcf2d86136a532287a", size = 289042, upload-time = "2025-10-06T05:37:49.499Z" }, + { url = "https://files.pythonhosted.org/packages/96/52/abddd34ca99be142f354398700536c5bd315880ed0a213812bc491cff5e4/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:974b28cf63cc99dfb2188d8d222bc6843656188164848c4f679e63dae4b0708e", size = 283148, upload-time = "2025-10-06T05:37:50.745Z" }, + { url = "https://files.pythonhosted.org/packages/af/d3/76bd4ed4317e7119c2b7f57c3f6934aba26d277acc6309f873341640e21f/frozenlist-1.8.0-cp314-cp314t-win32.whl", hash = "sha256:342c97bf697ac5480c0a7ec73cd700ecfa5a8a40ac923bd035484616efecc2df", size = 44676, upload-time = "2025-10-06T05:37:52.222Z" }, + { url = "https://files.pythonhosted.org/packages/89/76/c615883b7b521ead2944bb3480398cbb07e12b7b4e4d073d3752eb721558/frozenlist-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:06be8f67f39c8b1dc671f5d83aaefd3358ae5cdcf8314552c57e7ed3e6475bdd", size = 49451, upload-time = "2025-10-06T05:37:53.425Z" }, + { url = "https://files.pythonhosted.org/packages/e0/a3/5982da14e113d07b325230f95060e2169f5311b1017ea8af2a29b374c289/frozenlist-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:102e6314ca4da683dca92e3b1355490fed5f313b768500084fbe6371fddfdb79", size = 42507, upload-time = "2025-10-06T05:37:54.513Z" }, + { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" }, +] + [[package]] name = "greenlet" version = "3.5.4" @@ -88,7 +498,9 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/61/16/71eefcf68267bbf06a9b6bff57d0b222e49432326e85d74348b67694b8d4/greenlet-3.5.4-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:e883de250e299654b1f1680f72a1a9f9ba62c9bd1bce84099c90657349a8dfbb", size = 294266, upload-time = "2026-07-22T11:37:56.142Z" }, { url = "https://files.pythonhosted.org/packages/36/ea/a0b19adfc35d07e10acb626e9d22a3893b95f1309c42c4a20161dec16800/greenlet-3.5.4-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:32802705c2c1ff25e8237b3bdacf2594fa02be80af8a66703eb7853ea7e68686", size = 613712, upload-time = "2026-07-22T12:26:39.375Z" }, { url = "https://files.pythonhosted.org/packages/54/76/a121978b3337407d05a1ce5f79b4aa5998a43a9d8422f9726029b90b4471/greenlet-3.5.4-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:57aa201b351f7c7c75627c60d29e4d5b97a07d37efeb62b903466fca42c097d7", size = 625582, upload-time = "2026-07-22T12:29:00.814Z" }, + { url = "https://files.pythonhosted.org/packages/d1/4a/f301f1d85c69a86b90b5d581a73e8927bba4e79450037e6e2cbca05eb4fd/greenlet-3.5.4-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9667862a2e38ad379f11b845daeda22c8989186def44f06962c9c4c05e556da7", size = 633429, upload-time = "2026-07-22T12:43:42.073Z" }, { url = "https://files.pythonhosted.org/packages/34/c2/080f16cf870e929e592f55767f01d6c98d2ee83bfdc36c3b892f2d0459ab/greenlet-3.5.4-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c3fe76c2cac86b4f7a1e92865ac0a54384deb05c92986287c1a7110d9bd53071", size = 624663, upload-time = "2026-07-22T11:51:08.016Z" }, + { url = "https://files.pythonhosted.org/packages/6f/2e/26884072b0eb343a4d5fee903341bfe5171b32b7f14553886e2b6349135a/greenlet-3.5.4-cp311-cp311-manylinux_2_39_riscv64.whl", hash = "sha256:ae53534b5dec0f4c2ec26f898f538dc8ea1ca3ef2927d597a9439e40a09da937", size = 428238, upload-time = "2026-07-22T12:39:49.973Z" }, { url = "https://files.pythonhosted.org/packages/9e/bb/8f3ca88370b817369008faeceeee85970adc16c92a70a3e5fe5fea495a57/greenlet-3.5.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1e1a4a684b16c45ba324e60b32a4386a87722bcb815d2a149d2182f9b401ca72", size = 1585010, upload-time = "2026-07-22T12:25:02.539Z" }, { url = "https://files.pythonhosted.org/packages/51/c2/45877154689709ebce9a0b83c2235e6ca0f31577889b02af308c8cc5f8fb/greenlet-3.5.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e849e6e139b9671adeac505f72fc05f4af7fd1921faef40295e214fc3b361b59", size = 1651283, upload-time = "2026-07-22T11:51:10.408Z" }, { url = "https://files.pythonhosted.org/packages/cd/7d/8711a75cb61d85246277c07ff6e1a6504621ba473d808c11ad225ffca43f/greenlet-3.5.4-cp311-cp311-win_amd64.whl", hash = "sha256:dc418cf4c873357964d6624445ed09472e50def990c65dd4e76fc3ba8cd9cef6", size = 246434, upload-time = "2026-07-22T11:43:15.557Z" }, @@ -96,7 +508,9 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f3/04/81bd731d6d1e3a469d9a4c36f5eb069bcf0cbb2d5d342c9fec22245b91fc/greenlet-3.5.4-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:3d66250e8b09f182ede05490998c818b5961f7a3640332d44c4927caec7bbfe4", size = 295909, upload-time = "2026-07-22T11:38:09.261Z" }, { url = "https://files.pythonhosted.org/packages/cc/dd/f5f22903a6ae70f5ea328ed0beaec92ad903f0e3b7d2845133b354abc4b8/greenlet-3.5.4-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c90e930c9c192e5b3ee9fb8bcd920ea3926155e2e3ded39fc697323addecee17", size = 612011, upload-time = "2026-07-22T12:26:40.69Z" }, { url = "https://files.pythonhosted.org/packages/8e/10/92a4a88d12b915d74ea5b6d288e4afefda4771647caa34442c156f7a454f/greenlet-3.5.4-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:791fdfeeb9c6e0c7b10fa151bf110d2a6974866f13dcb5b1c7efae698245893a", size = 624299, upload-time = "2026-07-22T12:29:02.089Z" }, + { url = "https://files.pythonhosted.org/packages/6c/f9/03e26be3487c5238e81f2b84714959a86ea8515a869828cf41f4fc54b34e/greenlet-3.5.4-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b7c895310363f310361e0fe2072af85269d2a2a285cd04c0c59e79a5e3670dcf", size = 629603, upload-time = "2026-07-22T12:43:43.456Z" }, { url = "https://files.pythonhosted.org/packages/50/6d/0b14bb9db2989f32cd9fe7f76afedea01ee8bee3f87c07e69f24adfe7e63/greenlet-3.5.4-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f88193799d43dbf8c8a806d6405c9c52fe2af40bf75072a606357b33cc336c7f", size = 621541, upload-time = "2026-07-22T11:51:09.464Z" }, + { url = "https://files.pythonhosted.org/packages/57/6b/7c55ca72ef80d57c16c4a55210f82582622462dc4485799a30f4ec6f3372/greenlet-3.5.4-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:13b980043cb1b3134e81ea469da1250ddcc6bfe6d245bbaa59168d9cdc8f228f", size = 432554, upload-time = "2026-07-22T12:39:51.379Z" }, { url = "https://files.pythonhosted.org/packages/48/3d/25e9a2d9eb6b2e8b7ca4e80a3a26cb887cce6c8e0a87c921164f11bc5574/greenlet-3.5.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b7a5f095767c4493afcd06067f2bb3b8716e3f3f9e92b99c88e7e99f885b3d4d", size = 1581444, upload-time = "2026-07-22T12:25:03.818Z" }, { url = "https://files.pythonhosted.org/packages/b9/96/4c9bf2e2c408dcc0556edce69efa9f802e82223573c53240136a086821f1/greenlet-3.5.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:42afdc1ab5f66da8c586c32af9224a74a706b4f0ea0dc3a4188a0860a09c65c9", size = 1645842, upload-time = "2026-07-22T11:51:12.295Z" }, { url = "https://files.pythonhosted.org/packages/b5/41/303ecb26a3a56122c0f4d4073ee078881847bd6b6f463ae0ec57ec20223b/greenlet-3.5.4-cp312-cp312-win_amd64.whl", hash = "sha256:60149df8f462d1b230038e6590c23c3b4768bb5d6c022b3b6e82532b34b0b8a3", size = 247169, upload-time = "2026-07-22T11:38:19.893Z" }, @@ -104,7 +518,9 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c0/9a/e51225dcd58713f16ccbdcc501a8da21098ea14515b7870f1f94459e5ff5/greenlet-3.5.4-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:24e61b88cb7e1b1d794b32a10cc346ac779681d6d74ff137a3e0a444d2bf1f02", size = 294831, upload-time = "2026-07-22T11:38:53.389Z" }, { url = "https://files.pythonhosted.org/packages/9f/ea/de50a50fadf979713ab18b46f22ad5ff5f2dcfc637a3ebdecf669801e1a5/greenlet-3.5.4-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:870d730fec833f5a06906a32596cc099b9161594642a92a520b7a88911c95356", size = 614619, upload-time = "2026-07-22T12:26:42.282Z" }, { url = "https://files.pythonhosted.org/packages/db/c7/2aae27fea41205b8650294c301f042a2a4bb6155eea48c995b890a92f2c1/greenlet-3.5.4-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ec5ff0d1878df6af3bf9b638a5a92a7d5693291de77c91bff10fa48519c604ef", size = 627021, upload-time = "2026-07-22T12:29:03.445Z" }, + { url = "https://files.pythonhosted.org/packages/1b/80/fb4d4788bbc8e54761f1fc88533af9523a6e86299fa113d6e8a8503ed9fc/greenlet-3.5.4-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:07bd44616608d873d06735b63ef1a88191d6ca57c8d291d6559c71bc14c0893c", size = 632845, upload-time = "2026-07-22T12:43:45.19Z" }, { url = "https://files.pythonhosted.org/packages/eb/56/79fd826f9ccaae0b84e1b4ef68dabba5e105bb044ffcd448a0b782fcba9a/greenlet-3.5.4-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d84d993f6e575c950d91a23c1345d18fe1a4310d447bf630849d7809196b52f0", size = 624002, upload-time = "2026-07-22T11:51:11.391Z" }, + { url = "https://files.pythonhosted.org/packages/42/e3/6086fa578ebb72772722cdc4bcd628459814b42e0c2db1e3cbd6552b3271/greenlet-3.5.4-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:3529a8a933582ad19e224792cac7372489526576b75b4c124e8e4f29948f4861", size = 435053, upload-time = "2026-07-22T12:39:52.715Z" }, { url = "https://files.pythonhosted.org/packages/0a/1a/27319f97e731298513dcba1a2e91b63e9d8811d9de22130f960b129b1bf1/greenlet-3.5.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:58023945f421093de5e6fa108c0985a8659d43f49e0216da25099369a121bcbd", size = 1581533, upload-time = "2026-07-22T12:25:05.322Z" }, { url = "https://files.pythonhosted.org/packages/b1/6d/24240bf562e9786dd2799ee0a4a4dadb4ded22510f41b20245099159ac8c/greenlet-3.5.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bae2728e1897aa8df8cb1af38cd48b3a743aefe29372de7b8b7a9f532501e69f", size = 1645781, upload-time = "2026-07-22T11:51:14.805Z" }, { url = "https://files.pythonhosted.org/packages/c1/5a/442ab1a9ef7ca6bf7210e5397a95972206a91a31033a03c8900866a10039/greenlet-3.5.4-cp313-cp313-win_amd64.whl", hash = "sha256:ca5726c0b08ca35ae873557266a78b2c3f3b2b7d7401aa5ff886c2045dd0111c", size = 247133, upload-time = "2026-07-22T11:39:20.661Z" }, @@ -112,7 +528,9 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a5/a7/6ab1d4f9cd548d15ab90da29947f2076100130bb179b0bde59f795a459e3/greenlet-3.5.4-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:7e8afa5eac028f8140ceafe5ceec66e6aa127ddcb21452d2a564dcd2900b5f22", size = 295410, upload-time = "2026-07-22T11:40:35.747Z" }, { url = "https://files.pythonhosted.org/packages/cd/7a/422f63b4715cbc0b24385305407adf38b48f6bb68b3e6b04090e994d0f5a/greenlet-3.5.4-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:73b37afe369021423ea53dd3123e04bffa7e93ac64429b9f50835b2e4fcae7cf", size = 661286, upload-time = "2026-07-22T12:26:43.8Z" }, { url = "https://files.pythonhosted.org/packages/d0/31/5a1cac663bf5582190c5a714ef81364f03cde232227f39748f8ae4c11da5/greenlet-3.5.4-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3ef964f56dfcb6f9bbef2a190d9126795eac408716aeae47b5e7c73c32aafca9", size = 673517, upload-time = "2026-07-22T12:29:04.815Z" }, + { url = "https://files.pythonhosted.org/packages/9c/bf/250c2921c7b585dde12f5239e313ca2dcbc464d161ecca36e4e6ef21762d/greenlet-3.5.4-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:cef589bc65fae02d10bca2ac341191c5b33acc2967892ebf4fcbd10eabb7a74c", size = 677968, upload-time = "2026-07-22T12:43:46.788Z" }, { url = "https://files.pythonhosted.org/packages/15/4a/2a82a1e3f8aaca020853ac8d12211280ca2b231aa08ea39f636f1060c319/greenlet-3.5.4-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c53ff01a5c53a40f2c16820ebc56d7c61a77f5fbe009dadd96292d5682f80f8", size = 670917, upload-time = "2026-07-22T11:51:13.589Z" }, + { url = "https://files.pythonhosted.org/packages/18/40/10bfcf6513558d82f7b95dd728001c63bd388259fe27d3e30ae01f103430/greenlet-3.5.4-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:dfc41ae893d9ceaf22c824f2153a88b30651b20e8758c2cd9ac143f23640563c", size = 480643, upload-time = "2026-07-22T12:39:54.149Z" }, { url = "https://files.pythonhosted.org/packages/68/b0/e379a152b17bfdfa95795af4049e37c0fd1b4d81f020d426db104ed07c77/greenlet-3.5.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ecca4d80d55a01ad6b23b33262662956149fbb7b2c6be2910f1705921958cbf3", size = 1628478, upload-time = "2026-07-22T12:25:06.678Z" }, { url = "https://files.pythonhosted.org/packages/5e/43/bffdfa64f7317f954c5c1230b5dd5922676ce198689a68c1ac1ed4b1b1a5/greenlet-3.5.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2ffbc533e0eaf8e80d8471411646ab88fe58f641d508c0b02b24494479f4d9ec", size = 1692021, upload-time = "2026-07-22T11:51:17.008Z" }, { url = "https://files.pythonhosted.org/packages/d0/11/f799f9637e2c6e9b0b716015e339040598b058cf7654dfc0d67468b177ed/greenlet-3.5.4-cp314-cp314-win_amd64.whl", hash = "sha256:305f69e6c4523d7f6979ed001cff4e5853c063e5da04880296603aa0227e544c", size = 248031, upload-time = "2026-07-22T11:40:11.007Z" }, @@ -120,14 +538,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ec/69/35c62ed49c320cb4d98e14698ccca5467d3bfe683984172be9cb564d9ce3/greenlet-3.5.4-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:41ddab54e4b238f4a6c323f39b4e59e176affd5a94d461a9fb7583dac74240a3", size = 305571, upload-time = "2026-07-22T11:40:31.659Z" }, { url = "https://files.pythonhosted.org/packages/5b/6c/64d60216b3640dcb0b62d913dd9e0d80030c09115bb2e4ba70c95d10ca45/greenlet-3.5.4-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b3dabe3e2809013052c68bdf0b7fa5f5f2859c43a80803131ad61af9cabd7867", size = 672568, upload-time = "2026-07-22T12:26:45.298Z" }, { url = "https://files.pythonhosted.org/packages/5c/de/ba3ab0a96292e53039530333b0d2ae18d9e508f3a325cd7bf15f8172944c/greenlet-3.5.4-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:27d3f00718634d4520a3a150154ac5da36f257869d41321953375b90bfbbc72c", size = 680076, upload-time = "2026-07-22T12:29:06.125Z" }, + { url = "https://files.pythonhosted.org/packages/ae/db/24a10af12bf8e639cec46c38b9ce1a282543ba42ff4fb0b31a970f1ab603/greenlet-3.5.4-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:39169a11d87a6a263afda3e9a27d1df16d0f919d40a4837cc73986c9884c0dd8", size = 681690, upload-time = "2026-07-22T12:43:48.109Z" }, { url = "https://files.pythonhosted.org/packages/a5/be/aeada79083c6f1c15f45d77a332f9c441af263ee298e3eb17522cd337d22/greenlet-3.5.4-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cbd60b5763c6543c1827e48faaf14ea9bfbad245f52b1a4d76a2a2d8884c6c66", size = 676733, upload-time = "2026-07-22T11:51:16.027Z" }, + { url = "https://files.pythonhosted.org/packages/f4/60/44a2eca7b9fd71ae0fae7ff184da1cd3169d176652b97aa1cffcbb0ef961/greenlet-3.5.4-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:bd3d1145f603b2db19feb9078c2e6855eb7c67e15580c010ed815cee519b86fd", size = 510263, upload-time = "2026-07-22T12:39:55.678Z" }, { url = "https://files.pythonhosted.org/packages/e9/10/2392fc3a98948652ef5fd1e7275c04f861dd13f74b78a2b4309f4ee4d090/greenlet-3.5.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f00f910f0e7b35416c63b23ad78b769aeccfc1775f712b43c4ee525624a2eef7", size = 1637327, upload-time = "2026-07-22T12:25:07.879Z" }, { url = "https://files.pythonhosted.org/packages/55/c6/e7237a3dfa1f205ed0d9ea1e46d70bd2811b32d516266399fb59d28ab90a/greenlet-3.5.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:91c26423753b92caf41ab3f98fd547d7374d4d9fc2d85be041886c1579d9255e", size = 1697493, upload-time = "2026-07-22T11:51:19.214Z" }, { url = "https://files.pythonhosted.org/packages/55/e3/4ba8154ba2a3d43729e499f72471b4b5c993f3826d3e24da81d5f06d6572/greenlet-3.5.4-cp314-cp314t-win_amd64.whl", hash = "sha256:ee032b91fd8ec29ec6c4cea2b8c561b178435134bd0752c7334b94e9c736c132", size = 251637, upload-time = "2026-07-22T11:40:37.44Z" }, { url = "https://files.pythonhosted.org/packages/90/03/e3f96dfc100261a29545ddc8270cafe58f9195b6651466910e820910de77/greenlet-3.5.4-cp315-cp315-macosx_11_0_universal2.whl", hash = "sha256:178111881dd7a6c946471fda85485ec796e1043c2b939f694b096e2ecf986809", size = 296076, upload-time = "2026-07-22T11:39:38.364Z" }, { url = "https://files.pythonhosted.org/packages/a4/3d/da52d208e5c977bce8667e784729e584e38b5785f4c1ec0f4c836e9a1c42/greenlet-3.5.4-cp315-cp315-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d92df08dd65fede97fc37aad36c2e9dcda3b31c467f8e0c2c096456cb818e927", size = 666870, upload-time = "2026-07-22T12:26:46.691Z" }, { url = "https://files.pythonhosted.org/packages/2d/8a/7e6dee25cb8a8cf9b362c8e597cc269593378bd916f16c736c059a52e85a/greenlet-3.5.4-cp315-cp315-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:99e8f8c4ebc4fd80aa26c1280ae9ad43a0976e786349703a181cf0bae60413e5", size = 677678, upload-time = "2026-07-22T12:29:07.508Z" }, + { url = "https://files.pythonhosted.org/packages/51/a7/dafc7415d430b0a43a16396eb49ecb3b62fd720877fb259cc4dcfaf5f31e/greenlet-3.5.4-cp315-cp315-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1f17e362d78e37559e0506c5a7d066bdd45073c36a0127a543e8a0df27242ff3", size = 681428, upload-time = "2026-07-22T12:43:49.623Z" }, { url = "https://files.pythonhosted.org/packages/6c/21/5a38699fa45de749e3857d93b8f07e4c20489e77c2d35d915a2e1c456606/greenlet-3.5.4-cp315-cp315-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:394de08dad5ffcb1f50c2159d93e398d9d2da3ed437645eaa54771fa720db9f0", size = 676067, upload-time = "2026-07-22T11:51:18.163Z" }, + { url = "https://files.pythonhosted.org/packages/2e/d9/6298f3432de301d4718766cf934bd73c418c73f81fbb77247319364b0d96/greenlet-3.5.4-cp315-cp315-manylinux_2_39_riscv64.whl", hash = "sha256:cd320d998cbaa032932830448e39abf3c6a12901295e386e8114db926e10cffb", size = 487446, upload-time = "2026-07-22T12:39:57.044Z" }, { url = "https://files.pythonhosted.org/packages/5e/ba/863116ab8ff1ca7a729e327800268939d182db47aa433db70e216e7d9194/greenlet-3.5.4-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:c883d61f2282d72c767a14936641b3efcbde9d82f1080712aaea0b1d3126cb88", size = 1633489, upload-time = "2026-07-22T12:25:09.605Z" }, { url = "https://files.pythonhosted.org/packages/fa/06/7466ced82818d6132462d7f26b3f83c66ea15d2b193a6c0088d558ed7d95/greenlet-3.5.4-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:2a924f15d17957e252a810acefcb5942f5ca712298e8b6fcaed9a307d357522c", size = 1696584, upload-time = "2026-07-22T11:51:21.304Z" }, { url = "https://files.pythonhosted.org/packages/f9/4d/55b638489260065de9ffce606c8b5d04507bef705de4b212a0c3d6a1a0df/greenlet-3.5.4-cp315-cp315-win_amd64.whl", hash = "sha256:ed17e5f3420360d5b459de8462efb52060399a5326a613d4cde31cef63ef95da", size = 248297, upload-time = "2026-07-22T11:42:04.055Z" }, @@ -135,7 +557,9 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/19/66/7c87ed9cdbf1d49c2c6cd1c7b9dd4d16c33b24235ca03972293a1876b30c/greenlet-3.5.4-cp315-cp315t-macosx_11_0_universal2.whl", hash = "sha256:1833637f17d5e7472548a48575c394fe39f1b1890d676d162d86593610f44d8c", size = 306487, upload-time = "2026-07-22T11:41:25.118Z" }, { url = "https://files.pythonhosted.org/packages/5b/05/0a4201e7c0054866eefc05da234f236dd4c950d0fbf9ca0517141f01b269/greenlet-3.5.4-cp315-cp315t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:12cda9122e03341f1cb6b8207a19d7a9d375e52f1b4e9243918375f40fd7b4b9", size = 676479, upload-time = "2026-07-22T12:26:48.129Z" }, { url = "https://files.pythonhosted.org/packages/3d/c0/4b6b8c5a3aec70f0649cd89662d120fdd6421e2bdc8e3b15c3ab5ec568d8/greenlet-3.5.4-cp315-cp315t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d83ae0e32d14957ab7170785a20f582635c8474deab1bfbb552b17e769a6ce25", size = 684321, upload-time = "2026-07-22T12:29:08.925Z" }, + { url = "https://files.pythonhosted.org/packages/88/15/0b167aeea95285b0e654ddce651922f666c089363c2ec528ca8b9a9ba74f/greenlet-3.5.4-cp315-cp315t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:123aa379c962ed5fe90a880327e0c3066124ac64ec99e12a238be9fd8eb3db3d", size = 685995, upload-time = "2026-07-22T12:43:50.993Z" }, { url = "https://files.pythonhosted.org/packages/24/c9/b49c31c9a972eee91e260445770e922244a7efc542697f51a012ec046d0f/greenlet-3.5.4-cp315-cp315t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9f1467de1bb767f75db0aa34c195e3a496d8d1278c796e70c24ce205d3e99cde", size = 681293, upload-time = "2026-07-22T11:51:20.43Z" }, + { url = "https://files.pythonhosted.org/packages/de/90/c023ec337f32ff505be7db759c80d98f0532bb94d0c6fa13645efe9bee2e/greenlet-3.5.4-cp315-cp315t-manylinux_2_39_riscv64.whl", hash = "sha256:adf2244d7f69409925a8f22ed22cc5f93cdfe5c9dc87ff3476be2c2aaae61a05", size = 516928, upload-time = "2026-07-22T12:39:58.359Z" }, { url = "https://files.pythonhosted.org/packages/95/6f/7f2d4653770500eee667866016d42d7a68e3d3462f80df6b8e3fcd48a0eb/greenlet-3.5.4-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:0fa53040b78b578120eecdc0265e3f1051487cc425d11a2b7c761daadf4feaa8", size = 1642474, upload-time = "2026-07-22T12:25:10.819Z" }, { url = "https://files.pythonhosted.org/packages/5a/d8/8cba31036a4caae448087ba5d150660ab03b4a0f54d9150f6495a3be7262/greenlet-3.5.4-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:60e0bc961d367df506660e9ac0177a76bc6d81305300704b0977d1634f76efe2", size = 1701012, upload-time = "2026-07-22T11:51:23.17Z" }, { url = "https://files.pythonhosted.org/packages/e3/cd/3f77a4cce3bae631b08eb52f53a82a976669600337e21dfdba811cb50267/greenlet-3.5.4-cp315-cp315t-win_amd64.whl", hash = "sha256:f680e549edb3eaf21eea4e7fe101e15ec180c74b7879ab46adc080f22d4015d2", size = 251977, upload-time = "2026-07-22T11:41:38.125Z" }, @@ -240,6 +664,132 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, ] +[[package]] +name = "jmespath" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d3/59/322338183ecda247fb5d1763a6cbe46eff7222eaeebafd9fa65d4bf5cb11/jmespath-1.1.0.tar.gz", hash = "sha256:472c87d80f36026ae83c6ddd0f1d05d4e510134ed462851fd5f754c8c3cbb88d", size = 27377, upload-time = "2026-01-22T16:35:26.279Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/2f/967ba146e6d58cf6a652da73885f52fc68001525b4197effc174321d70b4/jmespath-1.1.0-py3-none-any.whl", hash = "sha256:a5663118de4908c91729bea0acadca56526eb2698e83de10cd116ae0f4e97c64", size = 20419, upload-time = "2026-01-22T16:35:24.919Z" }, +] + +[[package]] +name = "multidict" +version = "6.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1a/c2/c2d94cbe6ac1753f3fc980da97b3d930efe1da3af3c9f5125354436c073d/multidict-6.7.1.tar.gz", hash = "sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d", size = 102010, upload-time = "2026-01-26T02:46:45.979Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/f1/a90635c4f88fb913fbf4ce660b83b7445b7a02615bda034b2f8eb38fd597/multidict-6.7.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7ff981b266af91d7b4b3793ca3382e53229088d193a85dfad6f5f4c27fc73e5d", size = 76626, upload-time = "2026-01-26T02:43:26.485Z" }, + { url = "https://files.pythonhosted.org/packages/a6/9b/267e64eaf6fc637a15b35f5de31a566634a2740f97d8d094a69d34f524a4/multidict-6.7.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:844c5bca0b5444adb44a623fb0a1310c2f4cd41f402126bb269cd44c9b3f3e1e", size = 44706, upload-time = "2026-01-26T02:43:27.607Z" }, + { url = "https://files.pythonhosted.org/packages/dd/a4/d45caf2b97b035c57267791ecfaafbd59c68212004b3842830954bb4b02e/multidict-6.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f2a0a924d4c2e9afcd7ec64f9de35fcd96915149b2216e1cb2c10a56df483855", size = 44356, upload-time = "2026-01-26T02:43:28.661Z" }, + { url = "https://files.pythonhosted.org/packages/fd/d2/0a36c8473f0cbaeadd5db6c8b72d15bbceeec275807772bfcd059bef487d/multidict-6.7.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8be1802715a8e892c784c0197c2ace276ea52702a0ede98b6310c8f255a5afb3", size = 244355, upload-time = "2026-01-26T02:43:31.165Z" }, + { url = "https://files.pythonhosted.org/packages/5d/16/8c65be997fd7dd311b7d39c7b6e71a0cb449bad093761481eccbbe4b42a2/multidict-6.7.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2e2d2ed645ea29f31c4c7ea1552fcfd7cb7ba656e1eafd4134a6620c9f5fdd9e", size = 246433, upload-time = "2026-01-26T02:43:32.581Z" }, + { url = "https://files.pythonhosted.org/packages/01/fb/4dbd7e848d2799c6a026ec88ad39cf2b8416aa167fcc903baa55ecaa045c/multidict-6.7.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:95922cee9a778659e91db6497596435777bd25ed116701a4c034f8e46544955a", size = 225376, upload-time = "2026-01-26T02:43:34.417Z" }, + { url = "https://files.pythonhosted.org/packages/b6/8a/4a3a6341eac3830f6053062f8fbc9a9e54407c80755b3f05bc427295c2d0/multidict-6.7.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6b83cabdc375ffaaa15edd97eb7c0c672ad788e2687004990074d7d6c9b140c8", size = 257365, upload-time = "2026-01-26T02:43:35.741Z" }, + { url = "https://files.pythonhosted.org/packages/f7/a2/dd575a69c1aa206e12d27d0770cdf9b92434b48a9ef0cd0d1afdecaa93c4/multidict-6.7.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:38fb49540705369bab8484db0689d86c0a33a0a9f2c1b197f506b71b4b6c19b0", size = 254747, upload-time = "2026-01-26T02:43:36.976Z" }, + { url = "https://files.pythonhosted.org/packages/5a/56/21b27c560c13822ed93133f08aa6372c53a8e067f11fbed37b4adcdac922/multidict-6.7.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:439cbebd499f92e9aa6793016a8acaa161dfa749ae86d20960189f5398a19144", size = 246293, upload-time = "2026-01-26T02:43:38.258Z" }, + { url = "https://files.pythonhosted.org/packages/5a/a4/23466059dc3854763423d0ad6c0f3683a379d97673b1b89ec33826e46728/multidict-6.7.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6d3bc717b6fe763b8be3f2bee2701d3c8eb1b2a8ae9f60910f1b2860c82b6c49", size = 242962, upload-time = "2026-01-26T02:43:40.034Z" }, + { url = "https://files.pythonhosted.org/packages/1f/67/51dd754a3524d685958001e8fa20a0f5f90a6a856e0a9dcabff69be3dbb7/multidict-6.7.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:619e5a1ac57986dbfec9f0b301d865dddf763696435e2962f6d9cf2fdff2bb71", size = 237360, upload-time = "2026-01-26T02:43:41.752Z" }, + { url = "https://files.pythonhosted.org/packages/64/3f/036dfc8c174934d4b55d86ff4f978e558b0e585cef70cfc1ad01adc6bf18/multidict-6.7.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0b38ebffd9be37c1170d33bc0f36f4f262e0a09bc1aac1c34c7aa51a7293f0b3", size = 245940, upload-time = "2026-01-26T02:43:43.042Z" }, + { url = "https://files.pythonhosted.org/packages/3d/20/6214d3c105928ebc353a1c644a6ef1408bc5794fcb4f170bb524a3c16311/multidict-6.7.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:10ae39c9cfe6adedcdb764f5e8411d4a92b055e35573a2eaa88d3323289ef93c", size = 253502, upload-time = "2026-01-26T02:43:44.371Z" }, + { url = "https://files.pythonhosted.org/packages/b1/e2/c653bc4ae1be70a0f836b82172d643fcf1dade042ba2676ab08ec08bff0f/multidict-6.7.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:25167cc263257660290fba06b9318d2026e3c910be240a146e1f66dd114af2b0", size = 247065, upload-time = "2026-01-26T02:43:45.745Z" }, + { url = "https://files.pythonhosted.org/packages/c8/11/a854b4154cd3bd8b1fd375e8a8ca9d73be37610c361543d56f764109509b/multidict-6.7.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:128441d052254f42989ef98b7b6a6ecb1e6f708aa962c7984235316db59f50fa", size = 241870, upload-time = "2026-01-26T02:43:47.054Z" }, + { url = "https://files.pythonhosted.org/packages/13/bf/9676c0392309b5fdae322333d22a829715b570edb9baa8016a517b55b558/multidict-6.7.1-cp311-cp311-win32.whl", hash = "sha256:d62b7f64ffde3b99d06b707a280db04fb3855b55f5a06df387236051d0668f4a", size = 41302, upload-time = "2026-01-26T02:43:48.753Z" }, + { url = "https://files.pythonhosted.org/packages/c9/68/f16a3a8ba6f7b6dc92a1f19669c0810bd2c43fc5a02da13b1cbf8e253845/multidict-6.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:bdbf9f3b332abd0cdb306e7c2113818ab1e922dc84b8f8fd06ec89ed2a19ab8b", size = 45981, upload-time = "2026-01-26T02:43:49.921Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ad/9dd5305253fa00cd3c7555dbef69d5bf4133debc53b87ab8d6a44d411665/multidict-6.7.1-cp311-cp311-win_arm64.whl", hash = "sha256:b8c990b037d2fff2f4e33d3f21b9b531c5745b33a49a7d6dbe7a177266af44f6", size = 43159, upload-time = "2026-01-26T02:43:51.635Z" }, + { url = "https://files.pythonhosted.org/packages/8d/9c/f20e0e2cf80e4b2e4b1c365bf5fe104ee633c751a724246262db8f1a0b13/multidict-6.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a90f75c956e32891a4eda3639ce6dd86e87105271f43d43442a3aedf3cddf172", size = 76893, upload-time = "2026-01-26T02:43:52.754Z" }, + { url = "https://files.pythonhosted.org/packages/fe/cf/18ef143a81610136d3da8193da9d80bfe1cb548a1e2d1c775f26b23d024a/multidict-6.7.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fccb473e87eaa1382689053e4a4618e7ba7b9b9b8d6adf2027ee474597128cd", size = 45456, upload-time = "2026-01-26T02:43:53.893Z" }, + { url = "https://files.pythonhosted.org/packages/a9/65/1caac9d4cd32e8433908683446eebc953e82d22b03d10d41a5f0fefe991b/multidict-6.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0fa96985700739c4c7853a43c0b3e169360d6855780021bfc6d0f1ce7c123e7", size = 43872, upload-time = "2026-01-26T02:43:55.041Z" }, + { url = "https://files.pythonhosted.org/packages/cf/3b/d6bd75dc4f3ff7c73766e04e705b00ed6dbbaccf670d9e05a12b006f5a21/multidict-6.7.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cb2a55f408c3043e42b40cc8eecd575afa27b7e0b956dfb190de0f8499a57a53", size = 251018, upload-time = "2026-01-26T02:43:56.198Z" }, + { url = "https://files.pythonhosted.org/packages/fd/80/c959c5933adedb9ac15152e4067c702a808ea183a8b64cf8f31af8ad3155/multidict-6.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb0ce7b2a32d09892b3dd6cc44877a0d02a33241fafca5f25c8b6b62374f8b75", size = 258883, upload-time = "2026-01-26T02:43:57.499Z" }, + { url = "https://files.pythonhosted.org/packages/86/85/7ed40adafea3d4f1c8b916e3b5cc3a8e07dfcdcb9cd72800f4ed3ca1b387/multidict-6.7.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c3a32d23520ee37bf327d1e1a656fec76a2edd5c038bf43eddfa0572ec49c60b", size = 242413, upload-time = "2026-01-26T02:43:58.755Z" }, + { url = "https://files.pythonhosted.org/packages/d2/57/b8565ff533e48595503c785f8361ff9a4fde4d67de25c207cd0ba3befd03/multidict-6.7.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9c90fed18bffc0189ba814749fdcc102b536e83a9f738a9003e569acd540a733", size = 268404, upload-time = "2026-01-26T02:44:00.216Z" }, + { url = "https://files.pythonhosted.org/packages/e0/50/9810c5c29350f7258180dfdcb2e52783a0632862eb334c4896ac717cebcb/multidict-6.7.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:da62917e6076f512daccfbbde27f46fed1c98fee202f0559adec8ee0de67f71a", size = 269456, upload-time = "2026-01-26T02:44:02.202Z" }, + { url = "https://files.pythonhosted.org/packages/f3/8d/5e5be3ced1d12966fefb5c4ea3b2a5b480afcea36406559442c6e31d4a48/multidict-6.7.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfde23ef6ed9db7eaee6c37dcec08524cb43903c60b285b172b6c094711b3961", size = 256322, upload-time = "2026-01-26T02:44:03.56Z" }, + { url = "https://files.pythonhosted.org/packages/31/6e/d8a26d81ac166a5592782d208dd90dfdc0a7a218adaa52b45a672b46c122/multidict-6.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3758692429e4e32f1ba0df23219cd0b4fc0a52f476726fff9337d1a57676a582", size = 253955, upload-time = "2026-01-26T02:44:04.845Z" }, + { url = "https://files.pythonhosted.org/packages/59/4c/7c672c8aad41534ba619bcd4ade7a0dc87ed6b8b5c06149b85d3dd03f0cd/multidict-6.7.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:398c1478926eca669f2fd6a5856b6de9c0acf23a2cb59a14c0ba5844fa38077e", size = 251254, upload-time = "2026-01-26T02:44:06.133Z" }, + { url = "https://files.pythonhosted.org/packages/7b/bd/84c24de512cbafbdbc39439f74e967f19570ce7924e3007174a29c348916/multidict-6.7.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c102791b1c4f3ab36ce4101154549105a53dc828f016356b3e3bcae2e3a039d3", size = 252059, upload-time = "2026-01-26T02:44:07.518Z" }, + { url = "https://files.pythonhosted.org/packages/fa/ba/f5449385510825b73d01c2d4087bf6d2fccc20a2d42ac34df93191d3dd03/multidict-6.7.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a088b62bd733e2ad12c50dad01b7d0166c30287c166e137433d3b410add807a6", size = 263588, upload-time = "2026-01-26T02:44:09.382Z" }, + { url = "https://files.pythonhosted.org/packages/d7/11/afc7c677f68f75c84a69fe37184f0f82fce13ce4b92f49f3db280b7e92b3/multidict-6.7.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3d51ff4785d58d3f6c91bdbffcb5e1f7ddfda557727043aa20d20ec4f65e324a", size = 259642, upload-time = "2026-01-26T02:44:10.73Z" }, + { url = "https://files.pythonhosted.org/packages/2b/17/ebb9644da78c4ab36403739e0e6e0e30ebb135b9caf3440825001a0bddcb/multidict-6.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc5907494fccf3e7d3f94f95c91d6336b092b5fc83811720fae5e2765890dfba", size = 251377, upload-time = "2026-01-26T02:44:12.042Z" }, + { url = "https://files.pythonhosted.org/packages/ca/a4/840f5b97339e27846c46307f2530a2805d9d537d8b8bd416af031cad7fa0/multidict-6.7.1-cp312-cp312-win32.whl", hash = "sha256:28ca5ce2fd9716631133d0e9a9b9a745ad7f60bac2bccafb56aa380fc0b6c511", size = 41887, upload-time = "2026-01-26T02:44:14.245Z" }, + { url = "https://files.pythonhosted.org/packages/80/31/0b2517913687895f5904325c2069d6a3b78f66cc641a86a2baf75a05dcbb/multidict-6.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcee94dfbd638784645b066074b338bc9cc155d4b4bffa4adce1615c5a426c19", size = 46053, upload-time = "2026-01-26T02:44:15.371Z" }, + { url = "https://files.pythonhosted.org/packages/0c/5b/aba28e4ee4006ae4c7df8d327d31025d760ffa992ea23812a601d226e682/multidict-6.7.1-cp312-cp312-win_arm64.whl", hash = "sha256:ba0a9fb644d0c1a2194cf7ffb043bd852cea63a57f66fbd33959f7dae18517bf", size = 43307, upload-time = "2026-01-26T02:44:16.852Z" }, + { url = "https://files.pythonhosted.org/packages/f2/22/929c141d6c0dba87d3e1d38fbdf1ba8baba86b7776469f2bc2d3227a1e67/multidict-6.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2b41f5fed0ed563624f1c17630cb9941cf2309d4df00e494b551b5f3e3d67a23", size = 76174, upload-time = "2026-01-26T02:44:18.509Z" }, + { url = "https://files.pythonhosted.org/packages/c7/75/bc704ae15fee974f8fccd871305e254754167dce5f9e42d88a2def741a1d/multidict-6.7.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84e61e3af5463c19b67ced91f6c634effb89ef8bfc5ca0267f954451ed4bb6a2", size = 45116, upload-time = "2026-01-26T02:44:19.745Z" }, + { url = "https://files.pythonhosted.org/packages/79/76/55cd7186f498ed080a18440c9013011eb548f77ae1b297206d030eb1180a/multidict-6.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:935434b9853c7c112eee7ac891bc4cb86455aa631269ae35442cb316790c1445", size = 43524, upload-time = "2026-01-26T02:44:21.571Z" }, + { url = "https://files.pythonhosted.org/packages/e9/3c/414842ef8d5a1628d68edee29ba0e5bcf235dbfb3ccd3ea303a7fe8c72ff/multidict-6.7.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:432feb25a1cb67fe82a9680b4d65fb542e4635cb3166cd9c01560651ad60f177", size = 249368, upload-time = "2026-01-26T02:44:22.803Z" }, + { url = "https://files.pythonhosted.org/packages/f6/32/befed7f74c458b4a525e60519fe8d87eef72bb1e99924fa2b0f9d97a221e/multidict-6.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e82d14e3c948952a1a85503817e038cba5905a3352de76b9a465075d072fba23", size = 256952, upload-time = "2026-01-26T02:44:24.306Z" }, + { url = "https://files.pythonhosted.org/packages/03/d6/c878a44ba877f366630c860fdf74bfb203c33778f12b6ac274936853c451/multidict-6.7.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4cfb48c6ea66c83bcaaf7e4dfa7ec1b6bbcf751b7db85a328902796dfde4c060", size = 240317, upload-time = "2026-01-26T02:44:25.772Z" }, + { url = "https://files.pythonhosted.org/packages/68/49/57421b4d7ad2e9e60e25922b08ceb37e077b90444bde6ead629095327a6f/multidict-6.7.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1d540e51b7e8e170174555edecddbd5538105443754539193e3e1061864d444d", size = 267132, upload-time = "2026-01-26T02:44:27.648Z" }, + { url = "https://files.pythonhosted.org/packages/b7/fe/ec0edd52ddbcea2a2e89e174f0206444a61440b40f39704e64dc807a70bd/multidict-6.7.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:273d23f4b40f3dce4d6c8a821c741a86dec62cded82e1175ba3d99be128147ed", size = 268140, upload-time = "2026-01-26T02:44:29.588Z" }, + { url = "https://files.pythonhosted.org/packages/b0/73/6e1b01cbeb458807aa0831742232dbdd1fa92bfa33f52a3f176b4ff3dc11/multidict-6.7.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d624335fd4fa1c08a53f8b4be7676ebde19cd092b3895c421045ca87895b429", size = 254277, upload-time = "2026-01-26T02:44:30.902Z" }, + { url = "https://files.pythonhosted.org/packages/6a/b2/5fb8c124d7561a4974c342bc8c778b471ebbeb3cc17df696f034a7e9afe7/multidict-6.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:12fad252f8b267cc75b66e8fc51b3079604e8d43a75428ffe193cd9e2195dfd6", size = 252291, upload-time = "2026-01-26T02:44:32.31Z" }, + { url = "https://files.pythonhosted.org/packages/5a/96/51d4e4e06bcce92577fcd488e22600bd38e4fd59c20cb49434d054903bd2/multidict-6.7.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:03ede2a6ffbe8ef936b92cb4529f27f42be7f56afcdab5ab739cd5f27fb1cbf9", size = 250156, upload-time = "2026-01-26T02:44:33.734Z" }, + { url = "https://files.pythonhosted.org/packages/db/6b/420e173eec5fba721a50e2a9f89eda89d9c98fded1124f8d5c675f7a0c0f/multidict-6.7.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:90efbcf47dbe33dcf643a1e400d67d59abeac5db07dc3f27d6bdeae497a2198c", size = 249742, upload-time = "2026-01-26T02:44:35.222Z" }, + { url = "https://files.pythonhosted.org/packages/44/a3/ec5b5bd98f306bc2aa297b8c6f11a46714a56b1e6ef5ebda50a4f5d7c5fb/multidict-6.7.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c4b9bfc148f5a91be9244d6264c53035c8a0dcd2f51f1c3c6e30e30ebaa1c84", size = 262221, upload-time = "2026-01-26T02:44:36.604Z" }, + { url = "https://files.pythonhosted.org/packages/cd/f7/e8c0d0da0cd1e28d10e624604e1a36bcc3353aaebdfdc3a43c72bc683a12/multidict-6.7.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:401c5a650f3add2472d1d288c26deebc540f99e2fb83e9525007a74cd2116f1d", size = 258664, upload-time = "2026-01-26T02:44:38.008Z" }, + { url = "https://files.pythonhosted.org/packages/52/da/151a44e8016dd33feed44f730bd856a66257c1ee7aed4f44b649fb7edeb3/multidict-6.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:97891f3b1b3ffbded884e2916cacf3c6fc87b66bb0dde46f7357404750559f33", size = 249490, upload-time = "2026-01-26T02:44:39.386Z" }, + { url = "https://files.pythonhosted.org/packages/87/af/a3b86bf9630b732897f6fc3f4c4714b90aa4361983ccbdcd6c0339b21b0c/multidict-6.7.1-cp313-cp313-win32.whl", hash = "sha256:e1c5988359516095535c4301af38d8a8838534158f649c05dd1050222321bcb3", size = 41695, upload-time = "2026-01-26T02:44:41.318Z" }, + { url = "https://files.pythonhosted.org/packages/b2/35/e994121b0e90e46134673422dd564623f93304614f5d11886b1b3e06f503/multidict-6.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:960c83bf01a95b12b08fd54324a4eb1d5b52c88932b5cba5d6e712bb3ed12eb5", size = 45884, upload-time = "2026-01-26T02:44:42.488Z" }, + { url = "https://files.pythonhosted.org/packages/ca/61/42d3e5dbf661242a69c97ea363f2d7b46c567da8eadef8890022be6e2ab0/multidict-6.7.1-cp313-cp313-win_arm64.whl", hash = "sha256:563fe25c678aaba333d5399408f5ec3c383ca5b663e7f774dd179a520b8144df", size = 43122, upload-time = "2026-01-26T02:44:43.664Z" }, + { url = "https://files.pythonhosted.org/packages/6d/b3/e6b21c6c4f314bb956016b0b3ef2162590a529b84cb831c257519e7fde44/multidict-6.7.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:c76c4bec1538375dad9d452d246ca5368ad6e1c9039dadcf007ae59c70619ea1", size = 83175, upload-time = "2026-01-26T02:44:44.894Z" }, + { url = "https://files.pythonhosted.org/packages/fb/76/23ecd2abfe0957b234f6c960f4ade497f55f2c16aeb684d4ecdbf1c95791/multidict-6.7.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:57b46b24b5d5ebcc978da4ec23a819a9402b4228b8a90d9c656422b4bdd8a963", size = 48460, upload-time = "2026-01-26T02:44:46.106Z" }, + { url = "https://files.pythonhosted.org/packages/c4/57/a0ed92b23f3a042c36bc4227b72b97eca803f5f1801c1ab77c8a212d455e/multidict-6.7.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e954b24433c768ce78ab7929e84ccf3422e46deb45a4dc9f93438f8217fa2d34", size = 46930, upload-time = "2026-01-26T02:44:47.278Z" }, + { url = "https://files.pythonhosted.org/packages/b5/66/02ec7ace29162e447f6382c495dc95826bf931d3818799bbef11e8f7df1a/multidict-6.7.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3bd231490fa7217cc832528e1cd8752a96f0125ddd2b5749390f7c3ec8721b65", size = 242582, upload-time = "2026-01-26T02:44:48.604Z" }, + { url = "https://files.pythonhosted.org/packages/58/18/64f5a795e7677670e872673aca234162514696274597b3708b2c0d276cce/multidict-6.7.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:253282d70d67885a15c8a7716f3a73edf2d635793ceda8173b9ecc21f2fb8292", size = 250031, upload-time = "2026-01-26T02:44:50.544Z" }, + { url = "https://files.pythonhosted.org/packages/c8/ed/e192291dbbe51a8290c5686f482084d31bcd9d09af24f63358c3d42fd284/multidict-6.7.1-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b4c48648d7649c9335cf1927a8b87fa692de3dcb15faa676c6a6f1f1aabda43", size = 228596, upload-time = "2026-01-26T02:44:51.951Z" }, + { url = "https://files.pythonhosted.org/packages/1e/7e/3562a15a60cf747397e7f2180b0a11dc0c38d9175a650e75fa1b4d325e15/multidict-6.7.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:98bc624954ec4d2c7cb074b8eefc2b5d0ce7d482e410df446414355d158fe4ca", size = 257492, upload-time = "2026-01-26T02:44:53.902Z" }, + { url = "https://files.pythonhosted.org/packages/24/02/7d0f9eae92b5249bb50ac1595b295f10e263dd0078ebb55115c31e0eaccd/multidict-6.7.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1b99af4d9eec0b49927b4402bcbb58dea89d3e0db8806a4086117019939ad3dd", size = 255899, upload-time = "2026-01-26T02:44:55.316Z" }, + { url = "https://files.pythonhosted.org/packages/00/e3/9b60ed9e23e64c73a5cde95269ef1330678e9c6e34dd4eb6b431b85b5a10/multidict-6.7.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6aac4f16b472d5b7dc6f66a0d49dd57b0e0902090be16594dc9ebfd3d17c47e7", size = 247970, upload-time = "2026-01-26T02:44:56.783Z" }, + { url = "https://files.pythonhosted.org/packages/3e/06/538e58a63ed5cfb0bd4517e346b91da32fde409d839720f664e9a4ae4f9d/multidict-6.7.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:21f830fe223215dffd51f538e78c172ed7c7f60c9b96a2bf05c4848ad49921c3", size = 245060, upload-time = "2026-01-26T02:44:58.195Z" }, + { url = "https://files.pythonhosted.org/packages/b2/2f/d743a3045a97c895d401e9bd29aaa09b94f5cbdf1bd561609e5a6c431c70/multidict-6.7.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f5dd81c45b05518b9aa4da4aa74e1c93d715efa234fd3e8a179df611cc85e5f4", size = 235888, upload-time = "2026-01-26T02:44:59.57Z" }, + { url = "https://files.pythonhosted.org/packages/38/83/5a325cac191ab28b63c52f14f1131f3b0a55ba3b9aa65a6d0bf2a9b921a0/multidict-6.7.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:eb304767bca2bb92fb9c5bd33cedc95baee5bb5f6c88e63706533a1c06ad08c8", size = 243554, upload-time = "2026-01-26T02:45:01.054Z" }, + { url = "https://files.pythonhosted.org/packages/20/1f/9d2327086bd15da2725ef6aae624208e2ef828ed99892b17f60c344e57ed/multidict-6.7.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c9035dde0f916702850ef66460bc4239d89d08df4d02023a5926e7446724212c", size = 252341, upload-time = "2026-01-26T02:45:02.484Z" }, + { url = "https://files.pythonhosted.org/packages/e8/2c/2a1aa0280cf579d0f6eed8ee5211c4f1730bd7e06c636ba2ee6aafda302e/multidict-6.7.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:af959b9beeb66c822380f222f0e0a1889331597e81f1ded7f374f3ecb0fd6c52", size = 246391, upload-time = "2026-01-26T02:45:03.862Z" }, + { url = "https://files.pythonhosted.org/packages/e5/03/7ca022ffc36c5a3f6e03b179a5ceb829be9da5783e6fe395f347c0794680/multidict-6.7.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:41f2952231456154ee479651491e94118229844dd7226541788be783be2b5108", size = 243422, upload-time = "2026-01-26T02:45:05.296Z" }, + { url = "https://files.pythonhosted.org/packages/dc/1d/b31650eab6c5778aceed46ba735bd97f7c7d2f54b319fa916c0f96e7805b/multidict-6.7.1-cp313-cp313t-win32.whl", hash = "sha256:df9f19c28adcb40b6aae30bbaa1478c389efd50c28d541d76760199fc1037c32", size = 47770, upload-time = "2026-01-26T02:45:06.754Z" }, + { url = "https://files.pythonhosted.org/packages/ac/5b/2d2d1d522e51285bd61b1e20df8f47ae1a9d80839db0b24ea783b3832832/multidict-6.7.1-cp313-cp313t-win_amd64.whl", hash = "sha256:d54ecf9f301853f2c5e802da559604b3e95bb7a3b01a9c295c6ee591b9882de8", size = 53109, upload-time = "2026-01-26T02:45:08.044Z" }, + { url = "https://files.pythonhosted.org/packages/3d/a3/cc409ba012c83ca024a308516703cf339bdc4b696195644a7215a5164a24/multidict-6.7.1-cp313-cp313t-win_arm64.whl", hash = "sha256:5a37ca18e360377cfda1d62f5f382ff41f2b8c4ccb329ed974cc2e1643440118", size = 45573, upload-time = "2026-01-26T02:45:09.349Z" }, + { url = "https://files.pythonhosted.org/packages/91/cc/db74228a8be41884a567e88a62fd589a913708fcf180d029898c17a9a371/multidict-6.7.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8f333ec9c5eb1b7105e3b84b53141e66ca05a19a605368c55450b6ba208cb9ee", size = 75190, upload-time = "2026-01-26T02:45:10.651Z" }, + { url = "https://files.pythonhosted.org/packages/d5/22/492f2246bb5b534abd44804292e81eeaf835388901f0c574bac4eeec73c5/multidict-6.7.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a407f13c188f804c759fc6a9f88286a565c242a76b27626594c133b82883b5c2", size = 44486, upload-time = "2026-01-26T02:45:11.938Z" }, + { url = "https://files.pythonhosted.org/packages/f1/4f/733c48f270565d78b4544f2baddc2fb2a245e5a8640254b12c36ac7ac68e/multidict-6.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0e161ddf326db5577c3a4cc2d8648f81456e8a20d40415541587a71620d7a7d1", size = 43219, upload-time = "2026-01-26T02:45:14.346Z" }, + { url = "https://files.pythonhosted.org/packages/24/bb/2c0c2287963f4259c85e8bcbba9182ced8d7fca65c780c38e99e61629d11/multidict-6.7.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1e3a8bb24342a8201d178c3b4984c26ba81a577c80d4d525727427460a50c22d", size = 245132, upload-time = "2026-01-26T02:45:15.712Z" }, + { url = "https://files.pythonhosted.org/packages/a7/f9/44d4b3064c65079d2467888794dea218d1601898ac50222ab8a9a8094460/multidict-6.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97231140a50f5d447d3164f994b86a0bed7cd016e2682f8650d6a9158e14fd31", size = 252420, upload-time = "2026-01-26T02:45:17.293Z" }, + { url = "https://files.pythonhosted.org/packages/8b/13/78f7275e73fa17b24c9a51b0bd9d73ba64bb32d0ed51b02a746eb876abe7/multidict-6.7.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b10359683bd8806a200fd2909e7c8ca3a7b24ec1d8132e483d58e791d881048", size = 233510, upload-time = "2026-01-26T02:45:19.356Z" }, + { url = "https://files.pythonhosted.org/packages/4b/25/8167187f62ae3cbd52da7893f58cb036b47ea3fb67138787c76800158982/multidict-6.7.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:283ddac99f7ac25a4acadbf004cb5ae34480bbeb063520f70ce397b281859362", size = 264094, upload-time = "2026-01-26T02:45:20.834Z" }, + { url = "https://files.pythonhosted.org/packages/a1/e7/69a3a83b7b030cf283fb06ce074a05a02322359783424d7edf0f15fe5022/multidict-6.7.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:538cec1e18c067d0e6103aa9a74f9e832904c957adc260e61cd9d8cf0c3b3d37", size = 260786, upload-time = "2026-01-26T02:45:22.818Z" }, + { url = "https://files.pythonhosted.org/packages/fe/3b/8ec5074bcfc450fe84273713b4b0a0dd47c0249358f5d82eb8104ffe2520/multidict-6.7.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7eee46ccb30ff48a1e35bb818cc90846c6be2b68240e42a78599166722cea709", size = 248483, upload-time = "2026-01-26T02:45:24.368Z" }, + { url = "https://files.pythonhosted.org/packages/48/5a/d5a99e3acbca0e29c5d9cba8f92ceb15dce78bab963b308ae692981e3a5d/multidict-6.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa263a02f4f2dd2d11a7b1bb4362aa7cb1049f84a9235d31adf63f30143469a0", size = 248403, upload-time = "2026-01-26T02:45:25.982Z" }, + { url = "https://files.pythonhosted.org/packages/35/48/e58cd31f6c7d5102f2a4bf89f96b9cf7e00b6c6f3d04ecc44417c00a5a3c/multidict-6.7.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:2e1425e2f99ec5bd36c15a01b690a1a2456209c5deed58f95469ffb46039ccbb", size = 240315, upload-time = "2026-01-26T02:45:27.487Z" }, + { url = "https://files.pythonhosted.org/packages/94/33/1cd210229559cb90b6786c30676bb0c58249ff42f942765f88793b41fdce/multidict-6.7.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:497394b3239fc6f0e13a78a3e1b61296e72bf1c5f94b4c4eb80b265c37a131cd", size = 245528, upload-time = "2026-01-26T02:45:28.991Z" }, + { url = "https://files.pythonhosted.org/packages/64/f2/6e1107d226278c876c783056b7db43d800bb64c6131cec9c8dfb6903698e/multidict-6.7.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:233b398c29d3f1b9676b4b6f75c518a06fcb2ea0b925119fb2c1bc35c05e1601", size = 258784, upload-time = "2026-01-26T02:45:30.503Z" }, + { url = "https://files.pythonhosted.org/packages/4d/c1/11f664f14d525e4a1b5327a82d4de61a1db604ab34c6603bb3c2cc63ad34/multidict-6.7.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:93b1818e4a6e0930454f0f2af7dfce69307ca03cdcfb3739bf4d91241967b6c1", size = 251980, upload-time = "2026-01-26T02:45:32.603Z" }, + { url = "https://files.pythonhosted.org/packages/e1/9f/75a9ac888121d0c5bbd4ecf4eead45668b1766f6baabfb3b7f66a410e231/multidict-6.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f33dc2a3abe9249ea5d8360f969ec7f4142e7ac45ee7014d8f8d5acddf178b7b", size = 243602, upload-time = "2026-01-26T02:45:34.043Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e7/50bf7b004cc8525d80dbbbedfdc7aed3e4c323810890be4413e589074032/multidict-6.7.1-cp314-cp314-win32.whl", hash = "sha256:3ab8b9d8b75aef9df299595d5388b14530839f6422333357af1339443cff777d", size = 40930, upload-time = "2026-01-26T02:45:36.278Z" }, + { url = "https://files.pythonhosted.org/packages/e0/bf/52f25716bbe93745595800f36fb17b73711f14da59ed0bb2eba141bc9f0f/multidict-6.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:5e01429a929600e7dab7b166062d9bb54a5eed752384c7384c968c2afab8f50f", size = 45074, upload-time = "2026-01-26T02:45:37.546Z" }, + { url = "https://files.pythonhosted.org/packages/97/ab/22803b03285fa3a525f48217963da3a65ae40f6a1b6f6cf2768879e208f9/multidict-6.7.1-cp314-cp314-win_arm64.whl", hash = "sha256:4885cb0e817aef5d00a2e8451d4665c1808378dc27c2705f1bf4ef8505c0d2e5", size = 42471, upload-time = "2026-01-26T02:45:38.889Z" }, + { url = "https://files.pythonhosted.org/packages/e0/6d/f9293baa6146ba9507e360ea0292b6422b016907c393e2f63fc40ab7b7b5/multidict-6.7.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0458c978acd8e6ea53c81eefaddbbee9c6c5e591f41b3f5e8e194780fe026581", size = 82401, upload-time = "2026-01-26T02:45:40.254Z" }, + { url = "https://files.pythonhosted.org/packages/7a/68/53b5494738d83558d87c3c71a486504d8373421c3e0dbb6d0db48ad42ee0/multidict-6.7.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c0abd12629b0af3cf590982c0b413b1e7395cd4ec026f30986818ab95bfaa94a", size = 48143, upload-time = "2026-01-26T02:45:41.635Z" }, + { url = "https://files.pythonhosted.org/packages/37/e8/5284c53310dcdc99ce5d66563f6e5773531a9b9fe9ec7a615e9bc306b05f/multidict-6.7.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:14525a5f61d7d0c94b368a42cff4c9a4e7ba2d52e2672a7b23d84dc86fb02b0c", size = 46507, upload-time = "2026-01-26T02:45:42.99Z" }, + { url = "https://files.pythonhosted.org/packages/e4/fc/6800d0e5b3875568b4083ecf5f310dcf91d86d52573160834fb4bfcf5e4f/multidict-6.7.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17307b22c217b4cf05033dabefe68255a534d637c6c9b0cc8382718f87be4262", size = 239358, upload-time = "2026-01-26T02:45:44.376Z" }, + { url = "https://files.pythonhosted.org/packages/41/75/4ad0973179361cdf3a113905e6e088173198349131be2b390f9fa4da5fc6/multidict-6.7.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a7e590ff876a3eaf1c02a4dfe0724b6e69a9e9de6d8f556816f29c496046e59", size = 246884, upload-time = "2026-01-26T02:45:47.167Z" }, + { url = "https://files.pythonhosted.org/packages/c3/9c/095bb28b5da139bd41fb9a5d5caff412584f377914bd8787c2aa98717130/multidict-6.7.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5fa6a95dfee63893d80a34758cd0e0c118a30b8dcb46372bf75106c591b77889", size = 225878, upload-time = "2026-01-26T02:45:48.698Z" }, + { url = "https://files.pythonhosted.org/packages/07/d0/c0a72000243756e8f5a277b6b514fa005f2c73d481b7d9e47cd4568aa2e4/multidict-6.7.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a0543217a6a017692aa6ae5cc39adb75e587af0f3a82288b1492eb73dd6cc2a4", size = 253542, upload-time = "2026-01-26T02:45:50.164Z" }, + { url = "https://files.pythonhosted.org/packages/c0/6b/f69da15289e384ecf2a68837ec8b5ad8c33e973aa18b266f50fe55f24b8c/multidict-6.7.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f99fe611c312b3c1c0ace793f92464d8cd263cc3b26b5721950d977b006b6c4d", size = 252403, upload-time = "2026-01-26T02:45:51.779Z" }, + { url = "https://files.pythonhosted.org/packages/a2/76/b9669547afa5a1a25cd93eaca91c0da1c095b06b6d2d8ec25b713588d3a1/multidict-6.7.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9004d8386d133b7e6135679424c91b0b854d2d164af6ea3f289f8f2761064609", size = 244889, upload-time = "2026-01-26T02:45:53.27Z" }, + { url = "https://files.pythonhosted.org/packages/7e/a9/a50d2669e506dad33cfc45b5d574a205587b7b8a5f426f2fbb2e90882588/multidict-6.7.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e628ef0e6859ffd8273c69412a2465c4be4a9517d07261b33334b5ec6f3c7489", size = 241982, upload-time = "2026-01-26T02:45:54.919Z" }, + { url = "https://files.pythonhosted.org/packages/c5/bb/1609558ad8b456b4827d3c5a5b775c93b87878fd3117ed3db3423dfbce1b/multidict-6.7.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:841189848ba629c3552035a6a7f5bf3b02eb304e9fea7492ca220a8eda6b0e5c", size = 232415, upload-time = "2026-01-26T02:45:56.981Z" }, + { url = "https://files.pythonhosted.org/packages/d8/59/6f61039d2aa9261871e03ab9dc058a550d240f25859b05b67fd70f80d4b3/multidict-6.7.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ce1bbd7d780bb5a0da032e095c951f7014d6b0a205f8318308140f1a6aba159e", size = 240337, upload-time = "2026-01-26T02:45:58.698Z" }, + { url = "https://files.pythonhosted.org/packages/a1/29/fdc6a43c203890dc2ae9249971ecd0c41deaedfe00d25cb6564b2edd99eb/multidict-6.7.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b26684587228afed0d50cf804cc71062cc9c1cdf55051c4c6345d372947b268c", size = 248788, upload-time = "2026-01-26T02:46:00.862Z" }, + { url = "https://files.pythonhosted.org/packages/a9/14/a153a06101323e4cf086ecee3faadba52ff71633d471f9685c42e3736163/multidict-6.7.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9f9af11306994335398293f9958071019e3ab95e9a707dc1383a35613f6abcb9", size = 242842, upload-time = "2026-01-26T02:46:02.824Z" }, + { url = "https://files.pythonhosted.org/packages/41/5f/604ae839e64a4a6efc80db94465348d3b328ee955e37acb24badbcd24d83/multidict-6.7.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b4938326284c4f1224178a560987b6cf8b4d38458b113d9b8c1db1a836e640a2", size = 240237, upload-time = "2026-01-26T02:46:05.898Z" }, + { url = "https://files.pythonhosted.org/packages/5f/60/c3a5187bf66f6fb546ff4ab8fb5a077cbdd832d7b1908d4365c7f74a1917/multidict-6.7.1-cp314-cp314t-win32.whl", hash = "sha256:98655c737850c064a65e006a3df7c997cd3b220be4ec8fe26215760b9697d4d7", size = 48008, upload-time = "2026-01-26T02:46:07.468Z" }, + { url = "https://files.pythonhosted.org/packages/0c/f7/addf1087b860ac60e6f382240f64fb99f8bfb532bb06f7c542b83c29ca61/multidict-6.7.1-cp314-cp314t-win_amd64.whl", hash = "sha256:497bde6223c212ba11d462853cfa4f0ae6ef97465033e7dc9940cdb3ab5b48e5", size = 53542, upload-time = "2026-01-26T02:46:08.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/81/4629d0aa32302ef7b2ec65c75a728cc5ff4fa410c50096174c1632e70b3e/multidict-6.7.1-cp314-cp314t-win_arm64.whl", hash = "sha256:2bbd113e0d4af5db41d5ebfe9ccaff89de2120578164f86a5d17d5a576d1e5b2", size = 44719, upload-time = "2026-01-26T02:46:11.146Z" }, + { url = "https://files.pythonhosted.org/packages/81/08/7036c080d7117f28a4af526d794aab6a84463126db031b007717c1a6676e/multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", size = 12319, upload-time = "2026-01-26T02:46:44.004Z" }, +] + [[package]] name = "packaging" version = "26.2" @@ -254,12 +804,16 @@ name = "pineagents-demo-server" version = "0.1.0" source = { editable = "." } dependencies = [ + { name = "aioboto3" }, + { name = "aiosqlite" }, + { name = "asyncmy" }, { name = "fastapi" }, { name = "httpx" }, { name = "pydantic" }, { name = "pyjwt" }, { name = "python-multipart" }, - { name = "sqlalchemy" }, + { name = "redis" }, + { name = "sqlalchemy", extra = ["asyncio"] }, { name = "uvicorn", extra = ["standard"] }, ] @@ -272,12 +826,16 @@ dev = [ [package.metadata] requires-dist = [ + { name = "aioboto3", specifier = ">=13.0.0" }, + { name = "aiosqlite", specifier = ">=0.20.0" }, + { name = "asyncmy", specifier = ">=0.2.9" }, { name = "fastapi", specifier = ">=0.115.0" }, { name = "httpx", specifier = ">=0.27.0" }, { name = "pydantic", specifier = ">=2.7.0" }, { name = "pyjwt", specifier = ">=2.8.0" }, { name = "python-multipart", specifier = ">=0.0.12" }, - { name = "sqlalchemy", specifier = ">=2.0.30" }, + { name = "redis", specifier = ">=5.0.0" }, + { name = "sqlalchemy", extras = ["asyncio"], specifier = ">=2.0.30" }, { name = "uvicorn", extras = ["standard"], specifier = ">=0.30.0" }, ] @@ -297,6 +855,117 @@ wheels = [ { url = "https://files.pythonhosted.org/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 = "propcache" +version = "0.5.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/44/c87281c333769159c50594f22610f77398a47ccbfbbf23074e744e86f87c/propcache-0.5.2.tar.gz", hash = "sha256:01c4fc7480cd0598bb4b57022df55b9ca296da7fc5a8760bd8451a7e63a7d427", size = 50208, upload-time = "2026-05-08T21:02:12.199Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/f1/8a8cc1c2c7e7934ab77e0163414f736fadbc0f5e8dd9673b952355ac175b/propcache-0.5.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:74b70780220e2dd89175ca24b81b68b67c83db499ae611e7f2313cb329801c78", size = 90744, upload-time = "2026-05-08T20:59:45.799Z" }, + { url = "https://files.pythonhosted.org/packages/c2/f4/651b1225e976bd1a2ba5cfba0c29d096581c2636b437e3a9a7ab6276270a/propcache-0.5.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a4840ab0ae0216d952f4b53dc6d0b992bfc2bedbfe360bdd9b548bc184c08959", size = 52033, upload-time = "2026-05-08T20:59:47.408Z" }, + { url = "https://files.pythonhosted.org/packages/15/a8/8ede85d6aa1f79fc7dc2f8fd2c8d65920b8272c3892903c8a1affde48cfb/propcache-0.5.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c6844ba6364fb12f403928a82cfd295ab103a2b315c77c747b2dbe4a41894ea7", size = 52754, upload-time = "2026-05-08T20:59:49.202Z" }, + { url = "https://files.pythonhosted.org/packages/7d/fe/b3551b41bbc2f5b5bb088fc6920567cd43101253e68fbaa261339eb96fe1/propcache-0.5.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2293949b855ce597f2826452d17c2d545fb5622379c4ea6fdf525e9b8e8a2511", size = 57573, upload-time = "2026-05-08T20:59:50.778Z" }, + { url = "https://files.pythonhosted.org/packages/83/27/ab851ebd1b7172e3e161f5f8d39e315d54a91bea246f01f4d872d3376aef/propcache-0.5.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0fd59b5af35f74da48d905dcbad55449ba13be91823cb05a9bd590bbf5b61660", size = 60645, upload-time = "2026-05-08T20:59:52.227Z" }, + { url = "https://files.pythonhosted.org/packages/95/7d/466b3d18022e9897cbda9c735c493c5bd747d7a4c6f5ea1480b4cec434b6/propcache-0.5.2-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29f9309a2e42b0d273be006fdb4be2d6c39a47f6f57d8fb1cf9f81481df81b66", size = 61563, upload-time = "2026-05-08T20:59:53.866Z" }, + { url = "https://files.pythonhosted.org/packages/27/1b/16ab7f2cf2041da2f60d156ba64c2484eadf9168075b4ff43c3ef60045af/propcache-0.5.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5aaa2b923c1944ac8febd6609cb373540a5563e7cbcb0fd770f75dace2eb817b", size = 58888, upload-time = "2026-05-08T20:59:55.457Z" }, + { url = "https://files.pythonhosted.org/packages/0a/67/bb777ffd907633563bf35fd859c4ce97b0512c32f4633cf5d1eb7c33512b/propcache-0.5.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:66ea454f095ddf5b6b14f56c064c0941c4788be11e18d2464cf643bf7203ff67", size = 59253, upload-time = "2026-05-08T20:59:57.075Z" }, + { url = "https://files.pythonhosted.org/packages/b9/42/64f8d90b73fd9cdc1499b48057ff6d9cd2a98a25734c9bb62ecf07e87061/propcache-0.5.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:95f1e3f4760d404b13c9976c0229b2b49a3c8e2c62a9ce92efdd2b11ada75e3f", size = 57558, upload-time = "2026-05-08T20:59:58.602Z" }, + { url = "https://files.pythonhosted.org/packages/eb/02/dba5bc03c9041f2092ea55a449caf5dfe68352c6654511b29ba0654ddb69/propcache-0.5.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:85341b12b9d55bad0bded24cac341bb34289469e03a11f3f583ea1cc1db0326c", size = 55007, upload-time = "2026-05-08T20:59:59.837Z" }, + { url = "https://files.pythonhosted.org/packages/14/c0/43f649c7aa2a77a3b100d84e9dea3a483120ecb608bfe36ce49eaff517fe/propcache-0.5.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:26a4dca084132874e639895c3135dfad5eb20bae209f62d1aeb31b03e601c3c0", size = 60355, upload-time = "2026-05-08T21:00:01.144Z" }, + { url = "https://files.pythonhosted.org/packages/83/c0/435dafd27f1cb4a495381dae60e25883ccfe4020bb72818e8184c1678092/propcache-0.5.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:3b199b9b2b3d6a7edf3183ba8a9a137a22b97f7df525feb5ae1eccf026d2a9c6", size = 59057, upload-time = "2026-05-08T21:00:02.401Z" }, + { url = "https://files.pythonhosted.org/packages/53/ae/6e292df9135d659944e96cb3389258e4a663e5b2b5f6c217ef0ddc8d2f73/propcache-0.5.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e59bc9e66329185b93dab73f210f1a37f81cb40f321501db8017c9aea15dba27", size = 61938, upload-time = "2026-05-08T21:00:03.638Z" }, + { url = "https://files.pythonhosted.org/packages/0b/42/314ebc50d8159055411fd6b0bda322ff510e4b1f7d2e4927940ad0f6af20/propcache-0.5.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:552ffadf6ad409844bc5919c42a0a83d88314cedddaea0e41e80a8b8fffe881f", size = 59731, upload-time = "2026-05-08T21:00:04.881Z" }, + { url = "https://files.pythonhosted.org/packages/b8/9b/2da6dee38871c3c8772fabc2758325a5c9077d6d18c597737dc04dd884cd/propcache-0.5.2-cp311-cp311-win32.whl", hash = "sha256:cd416c1de191973c52ff1a12a57446bfc7642797b282d7caf2162d7d1b8aa9a0", size = 38966, upload-time = "2026-05-08T21:00:06.511Z" }, + { url = "https://files.pythonhosted.org/packages/42/4e/f17363fb58c0afe05b067361cb6d86ed2d29de6506779a27547c4d183075/propcache-0.5.2-cp311-cp311-win_amd64.whl", hash = "sha256:44e488ef40dbb452700b2b1f8188934121f6648f52c295055662d2191959ff82", size = 42135, upload-time = "2026-05-08T21:00:08.088Z" }, + { url = "https://files.pythonhosted.org/packages/c6/eb/6af6685077d22e8b33358d3c548e3282706a0b3cd85044ffba4e5dd08e3b/propcache-0.5.2-cp311-cp311-win_arm64.whl", hash = "sha256:54adaa85a22078d1e306304a40984dc5be99d599bf3dc0a24dc98f7daeab89ab", size = 38381, upload-time = "2026-05-08T21:00:09.692Z" }, + { url = "https://files.pythonhosted.org/packages/4a/cb/e27bc2b2737a0bb49962b275efa051e8f1c35a936df7d5139b6b658b7dc9/propcache-0.5.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:806719138ecd720339a12410fb9614ac9b2b2d3a5fdf8235d56981c36f4039ba", size = 95887, upload-time = "2026-05-08T21:00:11.277Z" }, + { url = "https://files.pythonhosted.org/packages/e6/13/b8ae04c59392f8d11c6cd9fb4011d1dc7c86b81225c770280300e259ffe1/propcache-0.5.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:db2b80ea58eab4f86b2beec3cc8b39e8ff9276ac20e96b7cce43c8ae84cd6b5a", size = 54654, upload-time = "2026-05-08T21:00:12.604Z" }, + { url = "https://files.pythonhosted.org/packages/2c/7d/49777a3e20b55863d4794384a38acd460c04157b0a00f8602b0d508b8431/propcache-0.5.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e5cbfac9f61484f7e9f3597775500cd3ebe8274e9b050c38f9525c77c97520bf", size = 55190, upload-time = "2026-05-08T21:00:13.935Z" }, + { url = "https://files.pythonhosted.org/packages/44/c7/085d0cd63062e84044e3f05797749c3f8e3938ff3aeb0eb2f69d43fafc91/propcache-0.5.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5dbc581d2814337da56222fab8dc5f161cd798a434e49bac27930aaef798e144", size = 59995, upload-time = "2026-05-08T21:00:15.526Z" }, + { url = "https://files.pythonhosted.org/packages/9c/42/32cf8e3009e92b2645cf1e944f701e8ea4e924dffde1ee26db860bcbf7e4/propcache-0.5.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:857187f381f88c8e2fa2fe56ab94879d011b883d5a2ee5a1b60a8cd2a06846d9", size = 63422, upload-time = "2026-05-08T21:00:16.824Z" }, + { url = "https://files.pythonhosted.org/packages/9e/1b/f112433f99fc979431b87a39ef169e3f8df070d99a72792c56d6937ac48b/propcache-0.5.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:178b4a2cdaac1818e2bf1c5a99b94383fa73ea5382e032a48dec07dc5668dc42", size = 64342, upload-time = "2026-05-08T21:00:18.362Z" }, + { url = "https://files.pythonhosted.org/packages/14/15/5574111ae50dd6e879456888c0eadd4c5a869959775854e18e18a6b345f3/propcache-0.5.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f328175a2cde1f0ff2c4ed8ce968b9dcfb55f3a7153f39e2957ed994da13476", size = 61639, upload-time = "2026-05-08T21:00:19.692Z" }, + { url = "https://files.pythonhosted.org/packages/cc/da/4d775080b1490c0ae604acda868bd71aabe3a89ed16f2aa4339eb8a283e7/propcache-0.5.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5671d09a36b06d0fd4a3da0fccbcae360e9b1570924171a15e9e0997f0249fba", size = 61588, upload-time = "2026-05-08T21:00:21.155Z" }, + { url = "https://files.pythonhosted.org/packages/04/ac/f076982cbe2195ee9cf32de5a1e46951d9fb399fc207f390562dd0fd8fb2/propcache-0.5.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:80168e2ebe4d3ec6599d10ad8f520304ae1cad9b6c5a95372aef1b66b7bfb53a", size = 60029, upload-time = "2026-05-08T21:00:22.713Z" }, + { url = "https://files.pythonhosted.org/packages/70/60/189be62e0dd898dce3b331e1b8c7a543cd3a405ac0c81fe8ee8a9d5d77e1/propcache-0.5.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:45f11346f884bc47444f6e6647131055844134c3175b629f84952e2b5cd62b64", size = 56774, upload-time = "2026-05-08T21:00:24.001Z" }, + { url = "https://files.pythonhosted.org/packages/ea/9e/93377b9c7939c1ffae98f878dee955efadfd638078bc86dbc21f9d52f651/propcache-0.5.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8e778ebd44ef4f66ed60a0416b06b489687db264a9c0b3620362f26489492913", size = 63532, upload-time = "2026-05-08T21:00:25.545Z" }, + { url = "https://files.pythonhosted.org/packages/14/f9/590ef6cfb9b8028d516d287812ece32bb0bc5f11fbb9c8bf6b2e6313fec8/propcache-0.5.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:c0cb9ed24c8964e172768d455a38254c2dd8a552905729ce006cad3d3dda59b1", size = 61592, upload-time = "2026-05-08T21:00:27.186Z" }, + { url = "https://files.pythonhosted.org/packages/b4/5e/70958b3034c297a630bba2f17ca7abc2d5f39a803ad7e370ab79d1ecd022/propcache-0.5.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:1d1ad32d9d4355e2be65574fd0bfd3677e7066b009cd5b9b2dee8aa6a6393b33", size = 64788, upload-time = "2026-05-08T21:00:28.8Z" }, + { url = "https://files.pythonhosted.org/packages/12/fd/77fe5936d8c3086ca9048f7f415f122ed82e53884a9ec193646b42deef06/propcache-0.5.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c80f4ba3e8f00189165999a742ee526ebeccedf6c3f7beb0c7df821e9772435a", size = 62514, upload-time = "2026-05-08T21:00:30.098Z" }, + { url = "https://files.pythonhosted.org/packages/cf/74/66bd798b5b3be70aa1b391f5cc9d6a0a5532d7fd3b19ec0b213e72e6ad9d/propcache-0.5.2-cp312-cp312-win32.whl", hash = "sha256:8c7972d8f193740d9175f0998ab38717e6cd322d5935c5b0fef8c0d323fd9031", size = 39018, upload-time = "2026-05-08T21:00:31.622Z" }, + { url = "https://files.pythonhosted.org/packages/61/7c/5c0d34aa3024694d6dcb9271cdbdd08c4e47c1c0ad95ec7e7bc74cdea145/propcache-0.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:d9ee8826a7d47863a08ac44e1a5f611a462eefc3a194b492da242128bec75b42", size = 42322, upload-time = "2026-05-08T21:00:32.918Z" }, + { url = "https://files.pythonhosted.org/packages/4d/91/875812f1a3feb20ceba818ef39fbe4d92f1081e04ac815c822496d0d038b/propcache-0.5.2-cp312-cp312-win_arm64.whl", hash = "sha256:2800a4a8ead6b28cccd1ec54b59346f0def7922ee1c7598e8499c733cfbb7c84", size = 38172, upload-time = "2026-05-08T21:00:35.124Z" }, + { url = "https://files.pythonhosted.org/packages/c5/09/f049e45385503fe67db75a6b6186a7b9f0c3930366dc960522c312a825b1/propcache-0.5.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:099aaf4b4d1a02265b92a977edf00b5c4f63b3b17ac6de39b0d637c9cac0188a", size = 94457, upload-time = "2026-05-08T21:00:36.355Z" }, + { url = "https://files.pythonhosted.org/packages/6b/65/83d1d05655baf63113731bd5a1008435e14f8d1e5a06cbe4ec5b23ad7a31/propcache-0.5.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:68ce1c44c7a813a7f71ea04315a8c7b330b63db99d059a797a4651bb6f69f117", size = 53835, upload-time = "2026-05-08T21:00:38.072Z" }, + { url = "https://files.pythonhosted.org/packages/a9/12/a6ba6482bb5ea3260c000c9b20881c95fa11c6b30173715668259f844ed7/propcache-0.5.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fc299c129490f55f254cd90be0deca4764e36e9a7c08b4aa588479a3bbed3098", size = 54545, upload-time = "2026-05-08T21:00:39.319Z" }, + { url = "https://files.pythonhosted.org/packages/a9/19/7fa086f5764c59ec8a8e157cd93aa8497acc00aba9dcdec56bfffb32602d/propcache-0.5.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a6ae2198be502c10f09b2516e7b5d019816924bc3183a43ce792a7bd6625e6f4", size = 59886, upload-time = "2026-05-08T21:00:40.621Z" }, + { url = "https://files.pythonhosted.org/packages/a1/e4/5d7663dc8235956c8f5281698a3af1d351d8820341ddd890f59d9a9127f2/propcache-0.5.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6041d31504dc1779d700e1edcfb08eea334b357620b06681a4eabb57a74e574e", size = 63261, upload-time = "2026-05-08T21:00:41.775Z" }, + { url = "https://files.pythonhosted.org/packages/4a/4a/15a03adee24d6350da4292caeac44c34c033d2afe5e87eb370f38854560f/propcache-0.5.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f7eabc04151c78a9f4d5bbb5f1faf571e4defeb4b585e0fe95b60ff2dbe4d3d7", size = 64184, upload-time = "2026-05-08T21:00:43.018Z" }, + { url = "https://files.pythonhosted.org/packages/8b/c6/979176efdaa3d239e36d503d5af63a0a773b36662ed8f52e5b6a6d9fd40e/propcache-0.5.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4db0ba63d693afd40d249bd93f842b5f144f8fcbb83de05660373bcf30517b1d", size = 61534, upload-time = "2026-05-08T21:00:44.507Z" }, + { url = "https://files.pythonhosted.org/packages/c8/22/63e8cd1bae4c2d2be6493b6b7d10566ddafad88137cfbc99964a1119853c/propcache-0.5.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1dbcf7675229b35d31abb6547d8ebc8c27a830ac3f9a794edff6254873ec7c0a", size = 61500, upload-time = "2026-05-08T21:00:45.796Z" }, + { url = "https://files.pythonhosted.org/packages/60/5a/28e5d9acbac1cc9ccb67045e8c1b943aa8d79fdf39c93bd73cacd68008ea/propcache-0.5.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d310c013aad2c72f1c3f2f8dd3279d460a858c551f97aeb8c63e4693cca7b4d2", size = 59994, upload-time = "2026-05-08T21:00:47.093Z" }, + { url = "https://files.pythonhosted.org/packages/f3/40/db650677f554a95b9c01a7c9d93d629e93a15562f5deb4573c9ee136fed2/propcache-0.5.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:06187263ddad280d05b4d8a8b3bb7d164cbebd469236544a42e6d9b28ac6a4fa", size = 56884, upload-time = "2026-05-08T21:00:48.376Z" }, + { url = "https://files.pythonhosted.org/packages/80/45/70b39b89516ff8b96bf732fa6fded8cef20f293cb1508690101c3c07ec51/propcache-0.5.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3115559b8effafd63b142ea5ed53d63a16ea6469cbc63dce4ee194b42db5d853", size = 63464, upload-time = "2026-05-08T21:00:49.954Z" }, + { url = "https://files.pythonhosted.org/packages/f9/e2/fa59d3a89eac5534293124af4f1d0d0ada091ce4a0ab4610ce03fd2bdd8d/propcache-0.5.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c60462af8e6dc30c35407c7237ea908d777b22862bbee27bc4699c0d8bcdc45a", size = 61588, upload-time = "2026-05-08T21:00:51.281Z" }, + { url = "https://files.pythonhosted.org/packages/0b/97/efb547a55c4bc7381cfb202d6a2239ac621045277bc1ea5dfd3a7f0516c0/propcache-0.5.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:40314bca9ac559716fe374094fc81c11dcc34b64fd6c585360f5775690505704", size = 64667, upload-time = "2026-05-08T21:00:52.602Z" }, + { url = "https://files.pythonhosted.org/packages/92/56/f5c7d9b4b7595d5127da38974d791b2153f3d1eae6c674af3583ace92ad3/propcache-0.5.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cfa21e036ce1e1db2be04ba3b85d2df1bb1702fa01932d984c5464c665228ff4", size = 62463, upload-time = "2026-05-08T21:00:54.303Z" }, + { url = "https://files.pythonhosted.org/packages/bd/3b/484a3a65fc9f9f60c41dcd17b428bace5389544e2c680994534a20755066/propcache-0.5.2-cp313-cp313-win32.whl", hash = "sha256:f156a3529f38063b6dbaf356e15602a7f95f8055b1295a438433a6386f10463d", size = 38621, upload-time = "2026-05-08T21:00:55.808Z" }, + { url = "https://files.pythonhosted.org/packages/1c/fd/3f0f10dba4dabad3bf53102be007abf55481067952bde0fdddff439e7c61/propcache-0.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:dfed59d0a5aeb01e242e66ff0300bc4a265a7c05f612d30016f0b60b1017d757", size = 41649, upload-time = "2026-05-08T21:00:57.061Z" }, + { url = "https://files.pythonhosted.org/packages/90/ec/6ce619cc32bb500a482f811f9cd509368b4e58e638d13f2c68f370d6b475/propcache-0.5.2-cp313-cp313-win_arm64.whl", hash = "sha256:ba338430e87ceb9c8f0cf754de38a9860560261e56c00376debd628698a7364f", size = 37636, upload-time = "2026-05-08T21:00:58.646Z" }, + { url = "https://files.pythonhosted.org/packages/1b/82/c1d268bbbf2ef981c5bf0fbbe746db617c66e3bcefe431a1aa8943fbe23a/propcache-0.5.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a592f5f3da71c8691c788c13cb6734b6d17663d2e1cb8caddf0673d01ef8847d", size = 98872, upload-time = "2026-05-08T21:00:59.889Z" }, + { url = "https://files.pythonhosted.org/packages/f4/d4/52c871e73e864e6b34c0e2d58ac1ec5ccd149497ddc7ad2137ae98323a35/propcache-0.5.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6a997d0489e9668a384fcfd5061b857aa5361de73191cac204d04b889cfbbafa", size = 56257, upload-time = "2026-05-08T21:01:01.195Z" }, + { url = "https://files.pythonhosted.org/packages/67/f0/9b90ca2a210b3d09bcfcd96ecd0f55545c091535abce2a45de2775cfd357/propcache-0.5.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:10734b5484ea113152ee25a91dccedf81631791805d2c9ccb054958e51842c94", size = 56696, upload-time = "2026-05-08T21:01:02.941Z" }, + { url = "https://files.pythonhosted.org/packages/9d/0e/6e9d4ba07c8e56e21ddec1e75f12148142b21ca83a51871babce095334f4/propcache-0.5.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cafca7e56c12bb02ae16d283742bef25a61122e9dab2b5b3f2ccbe589ce32164", size = 62378, upload-time = "2026-05-08T21:01:04.475Z" }, + { url = "https://files.pythonhosted.org/packages/65/19/c10badaa463dde8a27ce884f8ee2ec37e6035b7c9f5ff0c8f74f06f08dac/propcache-0.5.2-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f064f8d2b59177878b7615df1735cd8fe3462ed6be8c7b217d17a276489c2b7f", size = 65283, upload-time = "2026-05-08T21:01:05.959Z" }, + { url = "https://files.pythonhosted.org/packages/b0/b6/93bea99ca80e19cef6512a8580e5b7857bbe09422d9daa7fd4ef5723306c/propcache-0.5.2-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f78abfa8dfc32376fd1aacf597b2f2fbbe0ea751419aee718af5d4f82537ef8c", size = 66616, upload-time = "2026-05-08T21:01:07.228Z" }, + { url = "https://files.pythonhosted.org/packages/83/e4/5c7462e50625f051f37fb38b8224f7639f667184bbd34424ec83819bb1b7/propcache-0.5.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7467da8a9822bf1a55336f877340c5bcbd3c482afc43a99771169f74a26dedc", size = 63773, upload-time = "2026-05-08T21:01:08.514Z" }, + { url = "https://files.pythonhosted.org/packages/ca/b6/99238894047b13c823be25027e736626cd414a52a5e30d2c3347c2733529/propcache-0.5.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a6ddc6ac9e25de626c1f129c1b467d7ecd33ce2237d3fd0c4e429feef0a7ee1f", size = 63664, upload-time = "2026-05-08T21:01:09.874Z" }, + { url = "https://files.pythonhosted.org/packages/85/1e/a3a1a63116a2b8edb415a8bb9a6f0c34bd03830b1e18e8ce2904e1dc1cf4/propcache-0.5.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2f22cbbac9e26a8e864c0985ff1268d5d939d53d9d9411a9824279097e03a2cb", size = 62643, upload-time = "2026-05-08T21:01:11.132Z" }, + { url = "https://files.pythonhosted.org/packages/e4/03/893cf147de2fc6543c5eaa07ad833170e7e2a2385725bbebe8c0503723bb/propcache-0.5.2-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:fc76378c62a0f04d0cd82fbb1a2cd2d7e28fcb40d5873f28a6c44e388aaa2751", size = 59595, upload-time = "2026-05-08T21:01:12.387Z" }, + { url = "https://files.pythonhosted.org/packages/86/3b/04c1a2e12c57766568ba75ba72b3bf2042818d4c1425fab6fc07155c7cff/propcache-0.5.2-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:acd2c8edba48e31e58a363b8cf4e5c7db3b04b3f9e371f601df30d9b0d244836", size = 65711, upload-time = "2026-05-08T21:01:13.676Z" }, + { url = "https://files.pythonhosted.org/packages/1c/34/80f8d0099f8d6bacc4de1624c85672681c8cd1149ca2da0e38fd120b817f/propcache-0.5.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:452b5065457eb9991ec5eb38ff41d6cd4c991c9ac7c531c4d5849ae473a9a13f", size = 64247, upload-time = "2026-05-08T21:01:14.936Z" }, + { url = "https://files.pythonhosted.org/packages/f3/1a/8b08f3a5f1037e9e370c55883ceeeee0f6dd0416fb2d2d67b8bfc91f2a79/propcache-0.5.2-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:3430bb2bfe1331885c427745a751e774ee679fd4344f80b97bf879815fe8fa55", size = 67102, upload-time = "2026-05-08T21:01:16.281Z" }, + { url = "https://files.pythonhosted.org/packages/34/68/8bdb7bb7756d76e005490649d10e4a8369e610c74d619f71e1aedf889e9c/propcache-0.5.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:cef6cea3922890dd6c9654971001fa797b526c16ab5e1e46c05fd6f877be7568", size = 64964, upload-time = "2026-05-08T21:01:17.57Z" }, + { url = "https://files.pythonhosted.org/packages/0a/aa/50fb0b5d3968b61a510926ff8b8465f1d6e976b3ab74496d7a4b9fc42515/propcache-0.5.2-cp313-cp313t-win32.whl", hash = "sha256:72d61e16dd78228b58c5d47be830ff3da7e5f139abdf0aef9d86cde1c5cf2191", size = 42546, upload-time = "2026-05-08T21:01:18.946Z" }, + { url = "https://files.pythonhosted.org/packages/ae/4c/0ddbae64321bd4a95bcbfc19307238016b5b1fee645c84626c8d539e5b74/propcache-0.5.2-cp313-cp313t-win_amd64.whl", hash = "sha256:0958834041a0166d343b8d2cedcd8bcbaeb4fdbe0cf08320c5379f143c3be6e7", size = 46330, upload-time = "2026-05-08T21:01:20.162Z" }, + { url = "https://files.pythonhosted.org/packages/00/d9/9cddc8efb78d8af264c5ec9f6d10b62f57c515feda8d321595f56010fb23/propcache-0.5.2-cp313-cp313t-win_arm64.whl", hash = "sha256:6de8bd93ddde9b992cf2b2e0d796d501a19026b5b9fd87356d7d0779531a8d96", size = 40521, upload-time = "2026-05-08T21:01:21.399Z" }, + { url = "https://files.pythonhosted.org/packages/e2/ea/23ee535d90ce8bcc465a3028eb3cc0ce3bd1005f4bb27710b30587de798d/propcache-0.5.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:46088abff4cba581dea21ae0467a480526cb25aa5f3c269e909f800328bc3999", size = 94662, upload-time = "2026-05-08T21:01:22.683Z" }, + { url = "https://files.pythonhosted.org/packages/b5/06/c5a52f419b5d8972f8d46a7577476090d8e3263ff589ce40b5ca4968d5be/propcache-0.5.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fc88b26f08d634f7bc819a7852e5214f5802641ab8d9fd5326892292eee1993e", size = 53928, upload-time = "2026-05-08T21:01:23.986Z" }, + { url = "https://files.pythonhosted.org/packages/63/b1/4260d67d6bd85e58a66b72d54ce15d5de789b6f3870cc6bedf8ff9667401/propcache-0.5.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:97797ebb098e670a2f92dd66f32897e30d7615b14e7f59711de23e30a9072539", size = 54650, upload-time = "2026-05-08T21:01:25.305Z" }, + { url = "https://files.pythonhosted.org/packages/70/06/2f46c318e3307cd7a6a7481def374ce838c0fe20084b39dd54b0879d0e99/propcache-0.5.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba57fffe4ac99c5d30076161b5866336d97600769bad35cc68f7774b15298a4e", size = 59912, upload-time = "2026-05-08T21:01:26.545Z" }, + { url = "https://files.pythonhosted.org/packages/4c/29/fe1aebec2ce57ab985a9c382bded1124431f85078113aa222c5d278430d4/propcache-0.5.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:583c19759d9eec1e5b69e2fbef36a7d9c326041be9746cb822d335c8cedc2979", size = 63300, upload-time = "2026-05-08T21:01:27.937Z" }, + { url = "https://files.pythonhosted.org/packages/b4/18/2334b26768b6c82be8c69e83671b767d5ef426aa09b0cba6c2ea47816774/propcache-0.5.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d0326e2e5e1f3163fa306c834e48e8d490e5fae607a097a40c0648109b47ba80", size = 64208, upload-time = "2026-05-08T21:01:29.484Z" }, + { url = "https://files.pythonhosted.org/packages/2b/76/7f1bfd6afff4c5e38e36a3c6d68eb5f4b7311ea80baf693db78d95b603c4/propcache-0.5.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e00820e192c8dbebcafb383ebbf99030895f09905e7a0eb2e0340a0bcc2bc825", size = 61633, upload-time = "2026-05-08T21:01:31.068Z" }, + { url = "https://files.pythonhosted.org/packages/c4/46/b3ff8aba2b4953a3e50de2cf72f1b5748b8eca93b15f3dc2c84339084c09/propcache-0.5.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c66afea89b1e43725731d2004732a046fe6fe955d51f952c3e95a7314a284a39", size = 61724, upload-time = "2026-05-08T21:01:32.374Z" }, + { url = "https://files.pythonhosted.org/packages/c5/01/814cfcafbcff954f94c01cf30e097ddc88a076b5440fbcf4570753437d40/propcache-0.5.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d4dc37dec6c6cdad0b57881a5658fd14fbf53e333b1a86cf86559f190e1d9ec4", size = 60069, upload-time = "2026-05-08T21:01:33.67Z" }, + { url = "https://files.pythonhosted.org/packages/da/68/5c6f7622d510cc666a300687e06fd060c1a43361c0c9b20d284f06d8096a/propcache-0.5.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:5570dbcc97571c15f68068e529c92715a12f8d54030e272d264b377e22bd17a5", size = 57099, upload-time = "2026-05-08T21:01:34.915Z" }, + { url = "https://files.pythonhosted.org/packages/55/27/9cb0b4c679124085327957d42521c99dba04c88c90c3e55a6f0b633ebccc/propcache-0.5.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f814362777a9f841adddb200ecdf8f5cb1e5a3c4b7a86378edbd6ccb26edd702", size = 63391, upload-time = "2026-05-08T21:01:36.231Z" }, + { url = "https://files.pythonhosted.org/packages/f0/9d/7258aaa5bdf60fc6f27591eef6fe52768cb0beda7140be477c8b12c9794a/propcache-0.5.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:196913dea116aeb5a2ba95af4ddcb7ea85559ae07d8eee8751688310d09168c3", size = 61626, upload-time = "2026-05-08T21:01:37.545Z" }, + { url = "https://files.pythonhosted.org/packages/8e/0d/41c602003e8a9b16fe1e7eadf62c7bfba9d5474370b24200bf48b315f45f/propcache-0.5.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:6e7b8719005dd1175be4ab1cd25e9b98659a5e0347331506ec6760d2773a7fb5", size = 64781, upload-time = "2026-05-08T21:01:38.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f3/38e66b1856e9bd079deea015bc4a55f7767c0e4db2f7dcf69e7e680ba4ce/propcache-0.5.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:51f96d685ab16e88cab128cd37a52c5da540809c8b879fa047731bfcb4ad35a4", size = 62570, upload-time = "2026-05-08T21:01:40.415Z" }, + { url = "https://files.pythonhosted.org/packages/95/ca/bbfe9b910ce57dde8bb4876b4520fc02a4e89497c10de26be936758a3aaa/propcache-0.5.2-cp314-cp314-win32.whl", hash = "sha256:cc6fc3cc62e8501d3ed62894425040d2728ecddb1ed072737a5c70bd537aa9f0", size = 39436, upload-time = "2026-05-08T21:01:41.654Z" }, + { url = "https://files.pythonhosted.org/packages/61/d2/45c9defbaa1ea297035d9d4cce9e8f80daafbf19319c6007f157c6256ea9/propcache-0.5.2-cp314-cp314-win_amd64.whl", hash = "sha256:81e3a30b0bb60caa22033dd0f8a3618d1d67356212514f62c57db75cb0ef410c", size = 42373, upload-time = "2026-05-08T21:01:43.041Z" }, + { url = "https://files.pythonhosted.org/packages/44/68/9ea5103f41d5217d7d6ec24db90018e23aebec070c3f9a6e54d12b841fd8/propcache-0.5.2-cp314-cp314-win_arm64.whl", hash = "sha256:0d2c9bf8528f135dbb805ce027567e09164f7efa51a2be07458a2c0420f292d0", size = 38554, upload-time = "2026-05-08T21:01:44.336Z" }, + { url = "https://files.pythonhosted.org/packages/8a/81/fadf555f42d3b762eea8a53950b0489fdc0aa9da5f8ed9e10ce0a4e01b48/propcache-0.5.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:4bc8ff1feffc6a61c7002ffe84634c41b822e104990ae009f44a0834430070bb", size = 99395, upload-time = "2026-05-08T21:01:45.883Z" }, + { url = "https://files.pythonhosted.org/packages/f5/c9/c61e134a686949cf7971af3a390148b1156f7be81c73bc0cd12c873e2d48/propcache-0.5.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:79aa3ff0a9b566633b642fa9caf7e21ed1c13d6feca718187873f199e1514078", size = 56653, upload-time = "2026-05-08T21:01:47.307Z" }, + { url = "https://files.pythonhosted.org/packages/cb/73/daf935ea7048ddd7ec8eec5345b4a40b619d2d178b3c0a0900796bc3c794/propcache-0.5.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1b31822f4474c4036bae62de9402710051d431a606d6a0f907fec79935a071aa", size = 56914, upload-time = "2026-05-08T21:01:48.573Z" }, + { url = "https://files.pythonhosted.org/packages/79/9f/aba959b435ea18617edd7cf0a7ad0b9c574b8fc7e3d2cd55fb59cb255d33/propcache-0.5.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13fef48778b5a2a756523fdb781326b028ca75e32858b04f2cdd19f394564917", size = 62567, upload-time = "2026-05-08T21:01:49.903Z" }, + { url = "https://files.pythonhosted.org/packages/6c/a1/859942de9a791ff42f6141736f5b37749b8f53e65edfa49638c67dd67e6a/propcache-0.5.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8b73ab70f1a3351fbc71f663b3e645af6dd0329100c353081cf69c37433fc6fe", size = 65542, upload-time = "2026-05-08T21:01:51.204Z" }, + { url = "https://files.pythonhosted.org/packages/b5/61/315bc0fd6c0fc7f80a528b8afd209e5fc4a875ea79571b91b8f50f442907/propcache-0.5.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5538d2c13d93e4698af7e092b57bc7298fd35d1d58e656ae18f23ee0d0378e03", size = 66845, upload-time = "2026-05-08T21:01:52.539Z" }, + { url = "https://files.pythonhosted.org/packages/47/f7/9f8122e3132e8e354ac41975ef8f1099be7d5a16bc7ae562734e993665c0/propcache-0.5.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cd645f03898405cabe694fb8bc35241e3a9c332ec85627584fe3de201452b335", size = 63985, upload-time = "2026-05-08T21:01:53.847Z" }, + { url = "https://files.pythonhosted.org/packages/c8/54/c317819ec157cbf6f35df9df9657a6f82daf34d5faf15948b2f639c2192e/propcache-0.5.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a473b3440261e0c60706e732b2ed2f517857344fc21bf48fdfe211e2d98eb285", size = 63999, upload-time = "2026-05-08T21:01:55.179Z" }, + { url = "https://files.pythonhosted.org/packages/5a/56/387e3f7dfce0a9233df41fb888aa1c30222cb4bbbf09537c02dd9bd85fe2/propcache-0.5.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7afa37062e6650640e932e4cc9297d81f9f42d9944029cc386b8247dea4da837", size = 62779, upload-time = "2026-05-08T21:01:57.489Z" }, + { url = "https://files.pythonhosted.org/packages/a1/9c/596784cb5824ed61ee960d3f8655a3f0993e107c6e98ab6c818b7fb92ccb/propcache-0.5.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:8a90efd5777e996e42d568db9ac740b944d691e565cbfd31b2f7832f9184b2b8", size = 59796, upload-time = "2026-05-08T21:01:58.736Z" }, + { url = "https://files.pythonhosted.org/packages/c2/3d/1a6cfa1726a48542c1e8784a0761421476a5b68e09b7f36bf95eb954aaba/propcache-0.5.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:f19bb891234d72535764d703bfed1153cc34f4214d5bd7150aee1eec9e8f4366", size = 66023, upload-time = "2026-05-08T21:02:00.228Z" }, + { url = "https://files.pythonhosted.org/packages/e4/0e/05fd6990369477076e4e280bcb970de760fddf0161a46e988bc95f7940ec/propcache-0.5.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:32775082acd2d807ee3db715c7770d38767b817870acfa08c29e057f3c4d5b56", size = 64448, upload-time = "2026-05-08T21:02:01.888Z" }, + { url = "https://files.pythonhosted.org/packages/cd/86/5f8da315a4309c62c10c0b2516b17492d5d3bbe1bb862b96604db67e2a37/propcache-0.5.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9282fb1a3bccd038da9f768b927b24a0c753e466c086b7c4f3c6982851eefb2d", size = 67329, upload-time = "2026-05-08T21:02:03.484Z" }, + { url = "https://files.pythonhosted.org/packages/da/d3/3368efe79ab21f0cdf86ef49895811c9cc933131d4cde1f28a624e22e712/propcache-0.5.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cc49723e2f60d6b32a0f0b08a3fd6d13203c07f1cd9566cfce0f12a917c967a2", size = 65172, upload-time = "2026-05-08T21:02:04.745Z" }, + { url = "https://files.pythonhosted.org/packages/d5/07/127e8b0bacfb325396196f9d976a22453049b89b9b2b08477cc3145faa44/propcache-0.5.2-cp314-cp314t-win32.whl", hash = "sha256:2d7aa89ebca5acc98cba9d1472d976e394782f587bad6661003602a619fd1821", size = 43813, upload-time = "2026-05-08T21:02:06.025Z" }, + { url = "https://files.pythonhosted.org/packages/88/fb/46dad6c0ae49ed230ab1b16c890c2b6314e2403e6c412976f4a72d64a527/propcache-0.5.2-cp314-cp314t-win_amd64.whl", hash = "sha256:d447bb0b3054be5818458fbb171208b1d9ff11eba14e18ca18b90cbb45767370", size = 47764, upload-time = "2026-05-08T21:02:07.353Z" }, + { url = "https://files.pythonhosted.org/packages/e7/c4/a47d0a63aa309d10d59ede6e9d4cff03a344a79d1f0f4cd0cd74997b53e0/propcache-0.5.2-cp314-cp314t-win_arm64.whl", hash = "sha256:fe67a3d11cd9b4efabfa45c3d00ffba2b26811442a73a581a94b67c2b5faccf6", size = 41140, upload-time = "2026-05-08T21:02:09.065Z" }, + { url = "https://files.pythonhosted.org/packages/3a/ed/1cdcab6ba3d6ab7feca11fc14f0eeea80755bb53ef4e892079f31b10a25f/propcache-0.5.2-py3-none-any.whl", hash = "sha256:be1ddfcbb376e3de5d2e2db1d58d6d67463e6b4f9f040c000de8e300295465fe", size = 14036, upload-time = "2026-05-08T21:02:10.673Z" }, +] + [[package]] name = "pydantic" version = "2.13.4" @@ -461,6 +1130,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/03/e2/08a497ef684b88559c9cc5f4ad53a37e7b99e727094a86d6ea32536d5d3c/pytest_asyncio-1.4.0-py3-none-any.whl", hash = "sha256:933ca923a23075a87fb7070c0ec272a6848489824d887c85c812670932835aa1", size = 16930, upload-time = "2026-05-26T09:56:02.576Z" }, ] +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + [[package]] name = "python-dotenv" version = "1.2.2" @@ -534,6 +1215,39 @@ wheels = [ { url = "https://files.pythonhosted.org/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 = "redis" +version = "8.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "async-timeout", marker = "python_full_version < '3.11.3'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a8/99/604f0b666d4c616d891cf77ebb9db6bb21601344c051aebf1b72b9ff915f/redis-8.1.0.tar.gz", hash = "sha256:6e1a19beef9225c83efd689c7e6b7da2d5215b1f42cd13b7fc3714d0a09c7b25", size = 5254356, upload-time = "2026-07-30T08:51:00.269Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/66/9d/c5731f6e3608663d4d3656fd8d3aecee8b509c3082818f5a13eae925baea/redis-8.1.0-py3-none-any.whl", hash = "sha256:a4fe1aac3d3b3cc791d4b3d5931c5a956045dc951ee74d1c913ee3ac4d2ee9fb", size = 560618, upload-time = "2026-07-30T08:50:58.497Z" }, +] + +[[package]] +name = "s3transfer" +version = "0.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/62/74/8d69dcb7a9efe8baa2046891735e5dfe433ad558ae23d9e3c14c633d1d58/s3transfer-0.14.0.tar.gz", hash = "sha256:eff12264e7c8b4985074ccce27a3b38a485bb7f7422cc8046fee9be4983e4125", size = 151547, upload-time = "2025-09-09T19:23:31.089Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/f0/ae7ca09223a81a1d890b2557186ea015f6e0502e9b8cb8e1813f1d8cfa4e/s3transfer-0.14.0-py3-none-any.whl", hash = "sha256:ea3b790c7077558ed1f02a3072fb3cb992bbbd253392f4b6e9e8976941c7d456", size = 85712, upload-time = "2025-09-09T19:23:30.041Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + [[package]] name = "sqlalchemy" version = "2.0.51" @@ -582,6 +1296,11 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e2/22/dbf013a12ec759e54a34a119e9e217435b3f71b2dd5c61a7ade0a25dae87/sqlalchemy-2.0.51-py3-none-any.whl", hash = "sha256:bb024d8b621d0be75f4f44ecc7c950450026e76d66dc8f791bb5331d7fed59d5", size = 1944334, upload-time = "2026-06-15T16:09:22.418Z" }, ] +[package.optional-dependencies] +asyncio = [ + { name = "greenlet" }, +] + [[package]] name = "starlette" version = "1.3.1" @@ -616,6 +1335,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, ] +[[package]] +name = "urllib3" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, +] + [[package]] name = "uvicorn" version = "0.52.1" @@ -895,3 +1623,161 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/bf/e3/91e297e41381d9131f3142a9c1a50389fd96b98125ce9b81526fa4e14b9f/websockets-17.0.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:afbce6e3f0fac32dc87c2a0d84869d1a706460d64f39f3889386413e6e4d3d26", size = 213434, upload-time = "2026-07-31T11:31:24.707Z" }, { url = "https://files.pythonhosted.org/packages/09/ce/3929538b2b9918f5eee623fbf3346893973191f6df93f19bbda097bd7bb7/websockets-17.0.1-py3-none-any.whl", hash = "sha256:c6be9cba65c65cc76dfa3d4619e359ff02a4476c74e179b215236c11a0b32345", size = 206718, upload-time = "2026-07-31T11:31:26.037Z" }, ] + +[[package]] +name = "wrapt" +version = "1.17.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/95/8f/aeb76c5b46e273670962298c23e7ddde79916cb74db802131d49a85e4b7d/wrapt-1.17.3.tar.gz", hash = "sha256:f66eb08feaa410fe4eebd17f2a2c8e2e46d3476e9f8c783daa8e09e0faa666d0", size = 55547, upload-time = "2025-08-12T05:53:21.714Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/db/00e2a219213856074a213503fdac0511203dceefff26e1daa15250cc01a0/wrapt-1.17.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:273a736c4645e63ac582c60a56b0acb529ef07f78e08dc6bfadf6a46b19c0da7", size = 53482, upload-time = "2025-08-12T05:51:45.79Z" }, + { url = "https://files.pythonhosted.org/packages/5e/30/ca3c4a5eba478408572096fe9ce36e6e915994dd26a4e9e98b4f729c06d9/wrapt-1.17.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5531d911795e3f935a9c23eb1c8c03c211661a5060aab167065896bbf62a5f85", size = 38674, upload-time = "2025-08-12T05:51:34.629Z" }, + { url = "https://files.pythonhosted.org/packages/31/25/3e8cc2c46b5329c5957cec959cb76a10718e1a513309c31399a4dad07eb3/wrapt-1.17.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0610b46293c59a3adbae3dee552b648b984176f8562ee0dba099a56cfbe4df1f", size = 38959, upload-time = "2025-08-12T05:51:56.074Z" }, + { url = "https://files.pythonhosted.org/packages/5d/8f/a32a99fc03e4b37e31b57cb9cefc65050ea08147a8ce12f288616b05ef54/wrapt-1.17.3-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b32888aad8b6e68f83a8fdccbf3165f5469702a7544472bdf41f582970ed3311", size = 82376, upload-time = "2025-08-12T05:52:32.134Z" }, + { url = "https://files.pythonhosted.org/packages/31/57/4930cb8d9d70d59c27ee1332a318c20291749b4fba31f113c2f8ac49a72e/wrapt-1.17.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cccf4f81371f257440c88faed6b74f1053eef90807b77e31ca057b2db74edb1", size = 83604, upload-time = "2025-08-12T05:52:11.663Z" }, + { url = "https://files.pythonhosted.org/packages/a8/f3/1afd48de81d63dd66e01b263a6fbb86e1b5053b419b9b33d13e1f6d0f7d0/wrapt-1.17.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8a210b158a34164de8bb68b0e7780041a903d7b00c87e906fb69928bf7890d5", size = 82782, upload-time = "2025-08-12T05:52:12.626Z" }, + { url = "https://files.pythonhosted.org/packages/1e/d7/4ad5327612173b144998232f98a85bb24b60c352afb73bc48e3e0d2bdc4e/wrapt-1.17.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:79573c24a46ce11aab457b472efd8d125e5a51da2d1d24387666cd85f54c05b2", size = 82076, upload-time = "2025-08-12T05:52:33.168Z" }, + { url = "https://files.pythonhosted.org/packages/bb/59/e0adfc831674a65694f18ea6dc821f9fcb9ec82c2ce7e3d73a88ba2e8718/wrapt-1.17.3-cp311-cp311-win32.whl", hash = "sha256:c31eebe420a9a5d2887b13000b043ff6ca27c452a9a22fa71f35f118e8d4bf89", size = 36457, upload-time = "2025-08-12T05:53:03.936Z" }, + { url = "https://files.pythonhosted.org/packages/83/88/16b7231ba49861b6f75fc309b11012ede4d6b0a9c90969d9e0db8d991aeb/wrapt-1.17.3-cp311-cp311-win_amd64.whl", hash = "sha256:0b1831115c97f0663cb77aa27d381237e73ad4f721391a9bfb2fe8bc25fa6e77", size = 38745, upload-time = "2025-08-12T05:53:02.885Z" }, + { url = "https://files.pythonhosted.org/packages/9a/1e/c4d4f3398ec073012c51d1c8d87f715f56765444e1a4b11e5180577b7e6e/wrapt-1.17.3-cp311-cp311-win_arm64.whl", hash = "sha256:5a7b3c1ee8265eb4c8f1b7d29943f195c00673f5ab60c192eba2d4a7eae5f46a", size = 36806, upload-time = "2025-08-12T05:52:53.368Z" }, + { url = "https://files.pythonhosted.org/packages/9f/41/cad1aba93e752f1f9268c77270da3c469883d56e2798e7df6240dcb2287b/wrapt-1.17.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ab232e7fdb44cdfbf55fc3afa31bcdb0d8980b9b95c38b6405df2acb672af0e0", size = 53998, upload-time = "2025-08-12T05:51:47.138Z" }, + { url = "https://files.pythonhosted.org/packages/60/f8/096a7cc13097a1869fe44efe68dace40d2a16ecb853141394047f0780b96/wrapt-1.17.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9baa544e6acc91130e926e8c802a17f3b16fbea0fd441b5a60f5cf2cc5c3deba", size = 39020, upload-time = "2025-08-12T05:51:35.906Z" }, + { url = "https://files.pythonhosted.org/packages/33/df/bdf864b8997aab4febb96a9ae5c124f700a5abd9b5e13d2a3214ec4be705/wrapt-1.17.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6b538e31eca1a7ea4605e44f81a48aa24c4632a277431a6ed3f328835901f4fd", size = 39098, upload-time = "2025-08-12T05:51:57.474Z" }, + { url = "https://files.pythonhosted.org/packages/9f/81/5d931d78d0eb732b95dc3ddaeeb71c8bb572fb01356e9133916cd729ecdd/wrapt-1.17.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:042ec3bb8f319c147b1301f2393bc19dba6e176b7da446853406d041c36c7828", size = 88036, upload-time = "2025-08-12T05:52:34.784Z" }, + { url = "https://files.pythonhosted.org/packages/ca/38/2e1785df03b3d72d34fc6252d91d9d12dc27a5c89caef3335a1bbb8908ca/wrapt-1.17.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3af60380ba0b7b5aeb329bc4e402acd25bd877e98b3727b0135cb5c2efdaefe9", size = 88156, upload-time = "2025-08-12T05:52:13.599Z" }, + { url = "https://files.pythonhosted.org/packages/b3/8b/48cdb60fe0603e34e05cffda0b2a4adab81fd43718e11111a4b0100fd7c1/wrapt-1.17.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0b02e424deef65c9f7326d8c19220a2c9040c51dc165cddb732f16198c168396", size = 87102, upload-time = "2025-08-12T05:52:14.56Z" }, + { url = "https://files.pythonhosted.org/packages/3c/51/d81abca783b58f40a154f1b2c56db1d2d9e0d04fa2d4224e357529f57a57/wrapt-1.17.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:74afa28374a3c3a11b3b5e5fca0ae03bef8450d6aa3ab3a1e2c30e3a75d023dc", size = 87732, upload-time = "2025-08-12T05:52:36.165Z" }, + { url = "https://files.pythonhosted.org/packages/9e/b1/43b286ca1392a006d5336412d41663eeef1ad57485f3e52c767376ba7e5a/wrapt-1.17.3-cp312-cp312-win32.whl", hash = "sha256:4da9f45279fff3543c371d5ababc57a0384f70be244de7759c85a7f989cb4ebe", size = 36705, upload-time = "2025-08-12T05:53:07.123Z" }, + { url = "https://files.pythonhosted.org/packages/28/de/49493f962bd3c586ab4b88066e967aa2e0703d6ef2c43aa28cb83bf7b507/wrapt-1.17.3-cp312-cp312-win_amd64.whl", hash = "sha256:e71d5c6ebac14875668a1e90baf2ea0ef5b7ac7918355850c0908ae82bcb297c", size = 38877, upload-time = "2025-08-12T05:53:05.436Z" }, + { url = "https://files.pythonhosted.org/packages/f1/48/0f7102fe9cb1e8a5a77f80d4f0956d62d97034bbe88d33e94699f99d181d/wrapt-1.17.3-cp312-cp312-win_arm64.whl", hash = "sha256:604d076c55e2fdd4c1c03d06dc1a31b95130010517b5019db15365ec4a405fc6", size = 36885, upload-time = "2025-08-12T05:52:54.367Z" }, + { url = "https://files.pythonhosted.org/packages/fc/f6/759ece88472157acb55fc195e5b116e06730f1b651b5b314c66291729193/wrapt-1.17.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a47681378a0439215912ef542c45a783484d4dd82bac412b71e59cf9c0e1cea0", size = 54003, upload-time = "2025-08-12T05:51:48.627Z" }, + { url = "https://files.pythonhosted.org/packages/4f/a9/49940b9dc6d47027dc850c116d79b4155f15c08547d04db0f07121499347/wrapt-1.17.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:54a30837587c6ee3cd1a4d1c2ec5d24e77984d44e2f34547e2323ddb4e22eb77", size = 39025, upload-time = "2025-08-12T05:51:37.156Z" }, + { url = "https://files.pythonhosted.org/packages/45/35/6a08de0f2c96dcdd7fe464d7420ddb9a7655a6561150e5fc4da9356aeaab/wrapt-1.17.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:16ecf15d6af39246fe33e507105d67e4b81d8f8d2c6598ff7e3ca1b8a37213f7", size = 39108, upload-time = "2025-08-12T05:51:58.425Z" }, + { url = "https://files.pythonhosted.org/packages/0c/37/6faf15cfa41bf1f3dba80cd3f5ccc6622dfccb660ab26ed79f0178c7497f/wrapt-1.17.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6fd1ad24dc235e4ab88cda009e19bf347aabb975e44fd5c2fb22a3f6e4141277", size = 88072, upload-time = "2025-08-12T05:52:37.53Z" }, + { url = "https://files.pythonhosted.org/packages/78/f2/efe19ada4a38e4e15b6dff39c3e3f3f73f5decf901f66e6f72fe79623a06/wrapt-1.17.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ed61b7c2d49cee3c027372df5809a59d60cf1b6c2f81ee980a091f3afed6a2d", size = 88214, upload-time = "2025-08-12T05:52:15.886Z" }, + { url = "https://files.pythonhosted.org/packages/40/90/ca86701e9de1622b16e09689fc24b76f69b06bb0150990f6f4e8b0eeb576/wrapt-1.17.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:423ed5420ad5f5529db9ce89eac09c8a2f97da18eb1c870237e84c5a5c2d60aa", size = 87105, upload-time = "2025-08-12T05:52:17.914Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e0/d10bd257c9a3e15cbf5523025252cc14d77468e8ed644aafb2d6f54cb95d/wrapt-1.17.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e01375f275f010fcbf7f643b4279896d04e571889b8a5b3f848423d91bf07050", size = 87766, upload-time = "2025-08-12T05:52:39.243Z" }, + { url = "https://files.pythonhosted.org/packages/e8/cf/7d848740203c7b4b27eb55dbfede11aca974a51c3d894f6cc4b865f42f58/wrapt-1.17.3-cp313-cp313-win32.whl", hash = "sha256:53e5e39ff71b3fc484df8a522c933ea2b7cdd0d5d15ae82e5b23fde87d44cbd8", size = 36711, upload-time = "2025-08-12T05:53:10.074Z" }, + { url = "https://files.pythonhosted.org/packages/57/54/35a84d0a4d23ea675994104e667ceff49227ce473ba6a59ba2c84f250b74/wrapt-1.17.3-cp313-cp313-win_amd64.whl", hash = "sha256:1f0b2f40cf341ee8cc1a97d51ff50dddb9fcc73241b9143ec74b30fc4f44f6cb", size = 38885, upload-time = "2025-08-12T05:53:08.695Z" }, + { url = "https://files.pythonhosted.org/packages/01/77/66e54407c59d7b02a3c4e0af3783168fff8e5d61def52cda8728439d86bc/wrapt-1.17.3-cp313-cp313-win_arm64.whl", hash = "sha256:7425ac3c54430f5fc5e7b6f41d41e704db073309acfc09305816bc6a0b26bb16", size = 36896, upload-time = "2025-08-12T05:52:55.34Z" }, + { url = "https://files.pythonhosted.org/packages/02/a2/cd864b2a14f20d14f4c496fab97802001560f9f41554eef6df201cd7f76c/wrapt-1.17.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cf30f6e3c077c8e6a9a7809c94551203c8843e74ba0c960f4a98cd80d4665d39", size = 54132, upload-time = "2025-08-12T05:51:49.864Z" }, + { url = "https://files.pythonhosted.org/packages/d5/46/d011725b0c89e853dc44cceb738a307cde5d240d023d6d40a82d1b4e1182/wrapt-1.17.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e228514a06843cae89621384cfe3a80418f3c04aadf8a3b14e46a7be704e4235", size = 39091, upload-time = "2025-08-12T05:51:38.935Z" }, + { url = "https://files.pythonhosted.org/packages/2e/9e/3ad852d77c35aae7ddebdbc3b6d35ec8013af7d7dddad0ad911f3d891dae/wrapt-1.17.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5ea5eb3c0c071862997d6f3e02af1d055f381b1d25b286b9d6644b79db77657c", size = 39172, upload-time = "2025-08-12T05:51:59.365Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f7/c983d2762bcce2326c317c26a6a1e7016f7eb039c27cdf5c4e30f4160f31/wrapt-1.17.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:281262213373b6d5e4bb4353bc36d1ba4084e6d6b5d242863721ef2bf2c2930b", size = 87163, upload-time = "2025-08-12T05:52:40.965Z" }, + { url = "https://files.pythonhosted.org/packages/e4/0f/f673f75d489c7f22d17fe0193e84b41540d962f75fce579cf6873167c29b/wrapt-1.17.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc4a8d2b25efb6681ecacad42fca8859f88092d8732b170de6a5dddd80a1c8fa", size = 87963, upload-time = "2025-08-12T05:52:20.326Z" }, + { url = "https://files.pythonhosted.org/packages/df/61/515ad6caca68995da2fac7a6af97faab8f78ebe3bf4f761e1b77efbc47b5/wrapt-1.17.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:373342dd05b1d07d752cecbec0c41817231f29f3a89aa8b8843f7b95992ed0c7", size = 86945, upload-time = "2025-08-12T05:52:21.581Z" }, + { url = "https://files.pythonhosted.org/packages/d3/bd/4e70162ce398462a467bc09e768bee112f1412e563620adc353de9055d33/wrapt-1.17.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d40770d7c0fd5cbed9d84b2c3f2e156431a12c9a37dc6284060fb4bec0b7ffd4", size = 86857, upload-time = "2025-08-12T05:52:43.043Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b8/da8560695e9284810b8d3df8a19396a6e40e7518059584a1a394a2b35e0a/wrapt-1.17.3-cp314-cp314-win32.whl", hash = "sha256:fbd3c8319de8e1dc79d346929cd71d523622da527cca14e0c1d257e31c2b8b10", size = 37178, upload-time = "2025-08-12T05:53:12.605Z" }, + { url = "https://files.pythonhosted.org/packages/db/c8/b71eeb192c440d67a5a0449aaee2310a1a1e8eca41676046f99ed2487e9f/wrapt-1.17.3-cp314-cp314-win_amd64.whl", hash = "sha256:e1a4120ae5705f673727d3253de3ed0e016f7cd78dc463db1b31e2463e1f3cf6", size = 39310, upload-time = "2025-08-12T05:53:11.106Z" }, + { url = "https://files.pythonhosted.org/packages/45/20/2cda20fd4865fa40f86f6c46ed37a2a8356a7a2fde0773269311f2af56c7/wrapt-1.17.3-cp314-cp314-win_arm64.whl", hash = "sha256:507553480670cab08a800b9463bdb881b2edeed77dc677b0a5915e6106e91a58", size = 37266, upload-time = "2025-08-12T05:52:56.531Z" }, + { url = "https://files.pythonhosted.org/packages/77/ed/dd5cf21aec36c80443c6f900449260b80e2a65cf963668eaef3b9accce36/wrapt-1.17.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ed7c635ae45cfbc1a7371f708727bf74690daedc49b4dba310590ca0bd28aa8a", size = 56544, upload-time = "2025-08-12T05:51:51.109Z" }, + { url = "https://files.pythonhosted.org/packages/8d/96/450c651cc753877ad100c7949ab4d2e2ecc4d97157e00fa8f45df682456a/wrapt-1.17.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:249f88ed15503f6492a71f01442abddd73856a0032ae860de6d75ca62eed8067", size = 40283, upload-time = "2025-08-12T05:51:39.912Z" }, + { url = "https://files.pythonhosted.org/packages/d1/86/2fcad95994d9b572db57632acb6f900695a648c3e063f2cd344b3f5c5a37/wrapt-1.17.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a03a38adec8066d5a37bea22f2ba6bbf39fcdefbe2d91419ab864c3fb515454", size = 40366, upload-time = "2025-08-12T05:52:00.693Z" }, + { url = "https://files.pythonhosted.org/packages/64/0e/f4472f2fdde2d4617975144311f8800ef73677a159be7fe61fa50997d6c0/wrapt-1.17.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5d4478d72eb61c36e5b446e375bbc49ed002430d17cdec3cecb36993398e1a9e", size = 108571, upload-time = "2025-08-12T05:52:44.521Z" }, + { url = "https://files.pythonhosted.org/packages/cc/01/9b85a99996b0a97c8a17484684f206cbb6ba73c1ce6890ac668bcf3838fb/wrapt-1.17.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223db574bb38637e8230eb14b185565023ab624474df94d2af18f1cdb625216f", size = 113094, upload-time = "2025-08-12T05:52:22.618Z" }, + { url = "https://files.pythonhosted.org/packages/25/02/78926c1efddcc7b3aa0bc3d6b33a822f7d898059f7cd9ace8c8318e559ef/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e405adefb53a435f01efa7ccdec012c016b5a1d3f35459990afc39b6be4d5056", size = 110659, upload-time = "2025-08-12T05:52:24.057Z" }, + { url = "https://files.pythonhosted.org/packages/dc/ee/c414501ad518ac3e6fe184753632fe5e5ecacdcf0effc23f31c1e4f7bfcf/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:88547535b787a6c9ce4086917b6e1d291aa8ed914fdd3a838b3539dc95c12804", size = 106946, upload-time = "2025-08-12T05:52:45.976Z" }, + { url = "https://files.pythonhosted.org/packages/be/44/a1bd64b723d13bb151d6cc91b986146a1952385e0392a78567e12149c7b4/wrapt-1.17.3-cp314-cp314t-win32.whl", hash = "sha256:41b1d2bc74c2cac6f9074df52b2efbef2b30bdfe5f40cb78f8ca22963bc62977", size = 38717, upload-time = "2025-08-12T05:53:15.214Z" }, + { url = "https://files.pythonhosted.org/packages/79/d9/7cfd5a312760ac4dd8bf0184a6ee9e43c33e47f3dadc303032ce012b8fa3/wrapt-1.17.3-cp314-cp314t-win_amd64.whl", hash = "sha256:73d496de46cd2cdbdbcce4ae4bcdb4afb6a11234a1df9c085249d55166b95116", size = 41334, upload-time = "2025-08-12T05:53:14.178Z" }, + { url = "https://files.pythonhosted.org/packages/46/78/10ad9781128ed2f99dbc474f43283b13fea8ba58723e98844367531c18e9/wrapt-1.17.3-cp314-cp314t-win_arm64.whl", hash = "sha256:f38e60678850c42461d4202739f9bf1e3a737c7ad283638251e79cc49effb6b6", size = 38471, upload-time = "2025-08-12T05:52:57.784Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f6/a933bd70f98e9cf3e08167fc5cd7aaaca49147e48411c0bd5ae701bb2194/wrapt-1.17.3-py3-none-any.whl", hash = "sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22", size = 23591, upload-time = "2025-08-12T05:53:20.674Z" }, +] + +[[package]] +name = "yarl" +version = "1.24.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "multidict" }, + { name = "propcache" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/31/33/ebe9e3d1f86c7a0b51094c0a146392045ca1631d2664889539dec8088a33/yarl-1.24.5.tar.gz", hash = "sha256:e81b83143bee16329c23db3c1b2d82b29892fcbcb849186d2f6e98a5abe9a57f", size = 228679, upload-time = "2026-07-20T02:07:45.435Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/db/3cb5df059756a45761cc3dee8fd25ec82b83a6585ea3542b969fda850f99/yarl-1.24.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2c1fe720934a16ea8e7146175cba2126f87f54912c8c5435e7f7c7a51ef808d3", size = 135043, upload-time = "2026-07-20T02:04:52.39Z" }, + { url = "https://files.pythonhosted.org/packages/44/f8/767d6bd5a03db63bc467df2fb56d6fafeae9667d74aea92cd6af399f828b/yarl-1.24.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c687ed078e145f5fd53a14854beff320e1d2ab76df03e2009c98f39a0f68f39a", size = 96942, upload-time = "2026-07-20T02:04:54.26Z" }, + { url = "https://files.pythonhosted.org/packages/ce/97/10b939c44d7b28d1dbc389cfc7012306d1ea8dba01eaef44b39fffaee52a/yarl-1.24.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:709f1efed56c4a145793c046cd4939f9959bcd818979a787b77d8e09c57a0840", size = 97046, upload-time = "2026-07-20T02:04:56.638Z" }, + { url = "https://files.pythonhosted.org/packages/5b/7a/b410dbe39b6255c55fb2a2bcee96eb844d0789235ddc381a889a90dc72d6/yarl-1.24.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:874019bd513008b009f58657134e5d0c5e030b3559bd0553976837adf52fe966", size = 110512, upload-time = "2026-07-20T02:04:58.955Z" }, + { url = "https://files.pythonhosted.org/packages/83/c7/da591971f78a5617e1f21f5699858ebccd836fe181a6493788ffc91ba69b/yarl-1.24.5-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a4582acf7ef76482f6f511ebaf1946dae7f2e85ec4728b81a678c01df63bd723", size = 102454, upload-time = "2026-07-20T02:05:00.623Z" }, + { url = "https://files.pythonhosted.org/packages/c4/8e/73b0ed4de47289a78a96045d76d1cfe5e41848bf0da59ce25b2ec87ee05d/yarl-1.24.5-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2cabe6546e41dabe439999a23fcb5246e0c3b595b4315b96ef755252be90caeb", size = 117617, upload-time = "2026-07-20T02:05:02.325Z" }, + { url = "https://files.pythonhosted.org/packages/cf/14/b744747bc4f57a8d55bd744df463457524583e1e9f7538b5ace0346ab92e/yarl-1.24.5-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:17f57620f5475b3c69109376cc87e42a7af5db13c9398e4292772a706ff10780", size = 116135, upload-time = "2026-07-20T02:05:04.05Z" }, + { url = "https://files.pythonhosted.org/packages/66/ca/95aa4d0e5b7ea4f20e4d577c42d001ed9df207569fdb063cc5ed4ebb496b/yarl-1.24.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:570fec8fbd22b032733625f03f10b7ff023bc399213db15e72a7acaef28c2f4e", size = 111935, upload-time = "2026-07-20T02:05:05.738Z" }, + { url = "https://files.pythonhosted.org/packages/72/0d/d2ad8d6b147832d177a4e720ba1962fe686eb0913b74503b3eca094b8bba/yarl-1.24.5-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5fede79c6f73ff2c3ef822864cb1ada23196e62756df53bc6231d351a49516a2", size = 110010, upload-time = "2026-07-20T02:05:07.471Z" }, + { url = "https://files.pythonhosted.org/packages/50/18/eb335e4120903903f4865041355ae46256a2406eb2865bc24827f4f27b61/yarl-1.24.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8ccf9aca873b767977c73df497a85dbedee4ee086ae9ae49dc461333b9b79f58", size = 110058, upload-time = "2026-07-20T02:05:09.246Z" }, + { url = "https://files.pythonhosted.org/packages/44/70/97353add32c62ad6f206d948ac5a5ee84398225e534dc6ed6433d1b335b6/yarl-1.24.5-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:ad5d8201d310b031e6cd839d9bac2d4e5a01533ce5d3d5b50b7de1ef3af1de61", size = 103308, upload-time = "2026-07-20T02:05:11.31Z" }, + { url = "https://files.pythonhosted.org/packages/68/39/5e7398d4b6f6b3c9062823ebc60802df5b272e3fe9e788f9734c6ee46c85/yarl-1.24.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:841f0852f48fefea3b12c9dfec00704dfa3aef5215d0e3ce564bb3d7cd8d57c6", size = 116898, upload-time = "2026-07-20T02:05:13.099Z" }, + { url = "https://files.pythonhosted.org/packages/e4/c9/09e52f2239e8b96357eccca05915382e4ba5405ebfb623b6036040d99654/yarl-1.24.5-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:9baafc71b04f8f4bb0703b21d6fc9f0c30b346c636a532ff16ec8491a5ea4b1f", size = 109400, upload-time = "2026-07-20T02:05:14.821Z" }, + { url = "https://files.pythonhosted.org/packages/4b/6a/e94133d4c2d1a14d2384310bf3e79d9cf32c9d1eae1c6f034fb80d098fa1/yarl-1.24.5-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d897129df1a22b12aeed2c2c98df0785a2e8e6e0bde87b389491d0025c187077", size = 115934, upload-time = "2026-07-20T02:05:17.78Z" }, + { url = "https://files.pythonhosted.org/packages/4e/3c/34955ed967b976fc38edcbb6d538dee79dbda4cb7fc7f72a0907a7c78e0f/yarl-1.24.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:dd625535328fd9882374356269227670189adfcc6a2d90284f323c05862eecbd", size = 112178, upload-time = "2026-07-20T02:05:19.675Z" }, + { url = "https://files.pythonhosted.org/packages/f5/46/d7bd3a8859d47dcfaffd7127af7076032a7da278a9a02e17b5f37bfb6712/yarl-1.24.5-cp311-cp311-win_amd64.whl", hash = "sha256:f4239bbec5a3577ddb49e4b50aeb32d8e5792098262ae2f63723f916a29b1a25", size = 97544, upload-time = "2026-07-20T02:05:21.523Z" }, + { url = "https://files.pythonhosted.org/packages/01/69/c1bfd21e32c638974ea2c542a0b8c53ef1fa9eff336020f5d014f9503ff2/yarl-1.24.5-cp311-cp311-win_arm64.whl", hash = "sha256:3ac6aff147deb9c09461b2d4bbdf6256831198f5d8a23f5d37138213090b6d8a", size = 93359, upload-time = "2026-07-20T02:05:23.493Z" }, + { url = "https://files.pythonhosted.org/packages/1b/84/71d051c850b5af41d168c679d9eb67eb7c55283ac4ee131673edf134bc4e/yarl-1.24.5-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d693396e5aea78db03decd60aec9ece16c9b40ba00a587f089615ff4e718a81d", size = 136035, upload-time = "2026-07-20T02:05:25.489Z" }, + { url = "https://files.pythonhosted.org/packages/03/4d/8ad27f9a1b7e69313cca5d695b925b48efe51208d3490e0844bae97cabc0/yarl-1.24.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3363fcc96e665878946ad7a106b9a13eac0541766a690ef287c0232ac768b6ec", size = 97642, upload-time = "2026-07-20T02:05:27.429Z" }, + { url = "https://files.pythonhosted.org/packages/ea/b4/05b4131c407006cd1e410e9c6539f16a0945724677e5364447313c15ea3e/yarl-1.24.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9d399bdcfb4a0f659b9b3788bbc89babe63d9a6a65aacdf4d4e7065ff2e6316c", size = 97323, upload-time = "2026-07-20T02:05:29.441Z" }, + { url = "https://files.pythonhosted.org/packages/20/16/e618c875c73e0e39611f20a581b3d5e8d59b8857bf001bee3263044c6deb/yarl-1.24.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90333fd89b43c0d08ac85f3f1447593fc2c66de18c3d6378d7125ea118dc7a54", size = 107741, upload-time = "2026-07-20T02:05:31.367Z" }, + { url = "https://files.pythonhosted.org/packages/d9/9a/c4defeaf3ed33fcb346aacf9c6e971a8d4e2bde04a0310e79abb208e7965/yarl-1.24.5-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:665b0a2c463cc9423dd647e0bfd9f4ccc9b50f768c55304d5e9f80b177c1de12", size = 103570, upload-time = "2026-07-20T02:05:33.303Z" }, + { url = "https://files.pythonhosted.org/packages/5f/e7/0e0e0de5865ebd5914537ef486f36c727a59865c3ac0cf5ff1b32aececbf/yarl-1.24.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e006d3a974c4ee19512e5f058abedb6eef36a5e553c14812bdeba1758d812e6d", size = 115815, upload-time = "2026-07-20T02:05:35.292Z" }, + { url = "https://files.pythonhosted.org/packages/2b/27/ca56b700cb170aba25a3893b75355b213935657dc5714d2383354a270e62/yarl-1.24.5-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e7d42c531243450ef0d4d9c172e7ed6ef052640f195629065041b5add4e058d1", size = 116025, upload-time = "2026-07-20T02:05:37.503Z" }, + { url = "https://files.pythonhosted.org/packages/d6/d0/d56c859b8222116f5d68459199f48359e0bf121b6f65a69bf329b3602ba0/yarl-1.24.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f08c7513ecef5aad65687bfdf6bc601ae9fccd04a42904501f8f7141abad9eb9", size = 109835, upload-time = "2026-07-20T02:05:39.506Z" }, + { url = "https://files.pythonhosted.org/packages/70/a2/3a35557e4d1a79425040eba202ccaf08bdc8717680fc77e2498a1ad2e0a5/yarl-1.24.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6c95b17fe34ed802f17e205112e6e10db92275c34fee290aa9bdc55a9c724027", size = 108884, upload-time = "2026-07-20T02:05:41.584Z" }, + { url = "https://files.pythonhosted.org/packages/e4/35/ef4c26356b7913c68983bac2d72a4212b3347af551cb8d250b99b5ed7b7f/yarl-1.24.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:56b149b22de33b23b0c6077ab9518c6dcb538ad462e1830e68d06591ccf6e38b", size = 107308, upload-time = "2026-07-20T02:05:43.697Z" }, + { url = "https://files.pythonhosted.org/packages/d5/91/ff0dc66c2ccf3e0153ab97ff61eabab4400e6a5264af427ab30cd69f1857/yarl-1.24.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:a8fe66b8f300da93798025a785a5b90b42f3810dc2b72283ff84a41aaaebc293", size = 103646, upload-time = "2026-07-20T02:05:45.895Z" }, + { url = "https://files.pythonhosted.org/packages/74/f0/33b9271c7f881766359d58266fa0811d2e5210ed860e28da7dc6d7786344/yarl-1.24.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:377fe3732edbaf78ee74efdf2c9f49f6e99f20e7f9d2649fda3eb4badd77d76e", size = 115305, upload-time = "2026-07-20T02:05:47.832Z" }, + { url = "https://files.pythonhosted.org/packages/ef/65/fd79fb1868c4a80db8661091de525bf430f63c3bea1b20e8b6a84fc7d359/yarl-1.24.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:e8ffa78582120024f476a611d7befc123cee59e47e8309d470cf667d806e613b", size = 108404, upload-time = "2026-07-20T02:05:49.604Z" }, + { url = "https://files.pythonhosted.org/packages/ff/ba/dbabe6b262f17a816c70cfc09558dbf03ece3ec76684d02f911a3d3a189c/yarl-1.24.5-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:daba5e594f06114e37db186efd2dd916609071e59daca901a0a2e71f02b142ce", size = 115940, upload-time = "2026-07-20T02:05:51.741Z" }, + { url = "https://files.pythonhosted.org/packages/a5/43/fab2d1dad9d340a268cdde63756a123d069723efff6a372d123fa74a9517/yarl-1.24.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:65be18ec59496c13908f02a2472751d9ef840b4f3fb5726f129306bf6a2a7bba", size = 110006, upload-time = "2026-07-20T02:05:53.554Z" }, + { url = "https://files.pythonhosted.org/packages/c4/27/41eb51bbd1b8d89546b83897cfb0164f1e109304fd408dbb151b639eec0f/yarl-1.24.5-cp312-cp312-win_amd64.whl", hash = "sha256:a929d878fec099030c292803b31e5d5540a7b6a31e6a3cc76cb4685fc2a2f51b", size = 97618, upload-time = "2026-07-20T02:05:55.57Z" }, + { url = "https://files.pythonhosted.org/packages/3c/25/b2553764b3d65db711d8f45416351ec4f420847558eb669edcbcaadf5780/yarl-1.24.5-cp312-cp312-win_arm64.whl", hash = "sha256:7ce27823052e2013b597e0c738b13e7e36b8ccb9400df8959417b052ab0fd92c", size = 93018, upload-time = "2026-07-20T02:05:57.554Z" }, + { url = "https://files.pythonhosted.org/packages/e1/63/64ef361967cc983573149dc1515d531db5da8a4c92d22bb833d59e01b313/yarl-1.24.5-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:79af890482fc94648e8cde4c68620378f7fef60932710fa17a66abc039244da2", size = 135075, upload-time = "2026-07-20T02:05:59.671Z" }, + { url = "https://files.pythonhosted.org/packages/bb/89/55920fd853ce43e608adbc3962456f0d649d6bb15250dc2988321da0fe1c/yarl-1.24.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:46c2f213e23a04b93a392942d782eb9e413e6ef6bf7c8c53884e599a5c174dcb", size = 97225, upload-time = "2026-07-20T02:06:01.769Z" }, + { url = "https://files.pythonhosted.org/packages/15/f0/7688d3f2cfff7590df2af38ec46d969f4281a4dddb08a9ad2eafbcdddf98/yarl-1.24.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92ab3e11448f2ff7bf53c5a26eff0edc086898ec8b21fb154b85839ce1d88075", size = 96751, upload-time = "2026-07-20T02:06:03.676Z" }, + { url = "https://files.pythonhosted.org/packages/05/1a/a851a0f94aaaf379dd4f901bfc80f634280bec51eb260b47363e2a4cd62e/yarl-1.24.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ebb0ec7f17803063d5aeb982f3b1bd2b2f4e4fae6751226cbd6ba1fcfe9e63ff", size = 107960, upload-time = "2026-07-20T02:06:05.699Z" }, + { url = "https://files.pythonhosted.org/packages/6c/a8/faea066c12f9c77ca0de90641f1655f9dd7b412477bf28c76d692f3aecff/yarl-1.24.5-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:82632daed195dcc8ea664e8556dc9bdbd671960fb3776bd92806ce05792c2448", size = 103500, upload-time = "2026-07-20T02:06:07.556Z" }, + { url = "https://files.pythonhosted.org/packages/fb/9c/1e67084c2a6e2f2db0e3be798328cb3be42c0119b621d25461479a224d21/yarl-1.24.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:53e549287ef628fecba270045c9701b0c564563a9b0577d24a4ec75b8ab8040f", size = 115780, upload-time = "2026-07-20T02:06:09.599Z" }, + { url = "https://files.pythonhosted.org/packages/58/86/1f94664e147474337e3359f52012cf3d02f825f694317b178bfba1078c62/yarl-1.24.5-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fcd3b77e2f17bbe4ca56ec7bcb07992647d19d0b9c05d84886dcd6f9eb810afd", size = 115308, upload-time = "2026-07-20T02:06:11.352Z" }, + { url = "https://files.pythonhosted.org/packages/0a/43/8e55ae7538ba5f28ccb3c845c6dd4549cf7016d5992e5326512519107cdd/yarl-1.24.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d46b86567dd4e248c6c159fcbcdcce01e0a5c8a7cd2334a0fff759d0fa075b16", size = 110574, upload-time = "2026-07-20T02:06:13.129Z" }, + { url = "https://files.pythonhosted.org/packages/ce/ba/a889ec8765cedcf2ac44dcb02d6a21e4861399b243b263c5f2dde27ee740/yarl-1.24.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7f72c74aa99359e27a2ee8d6613fefa28b5f76a983c083074dfc2aaa4ab46213", size = 109914, upload-time = "2026-07-20T02:06:15.243Z" }, + { url = "https://files.pythonhosted.org/packages/9c/c3/e45f821af67b791c2dbbe4a9f4137a1d33f8d386654a05a0c3f47bdfa25d/yarl-1.24.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3f45789ce415a7ec0820dc4f82925f9b5f7732070be1dec1f5f23ec381435a24", size = 107712, upload-time = "2026-07-20T02:06:17.443Z" }, + { url = "https://files.pythonhosted.org/packages/02/00/2ab0f42c9857fcb490bfaa6647b14540b53d241ab209f23220b958cc5832/yarl-1.24.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:6e73e7fe93f17a7b191f52ec9da9dd8c06a8fe735a1ecbd13b97d1c723bff385", size = 104251, upload-time = "2026-07-20T02:06:19.259Z" }, + { url = "https://files.pythonhosted.org/packages/7a/70/709d9a286e98af2c7fd8e4e6cada658b5c0e30d87dd7e2a63c2fb5767217/yarl-1.24.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4a36f9becdd4c5c52a20c3e9484128b070b1dcfc8944c006f3a528295a359a9c", size = 115319, upload-time = "2026-07-20T02:06:21.207Z" }, + { url = "https://files.pythonhosted.org/packages/5c/6c/3eaa515142991fe84cfc483ff986492211f1978f90161ccefdbec919d09b/yarl-1.24.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:7bcbe0fcf850eae67b6b01749815a4f7161c560a844c769ad7b48fcd99f791c4", size = 109163, upload-time = "2026-07-20T02:06:23.006Z" }, + { url = "https://files.pythonhosted.org/packages/bb/64/711dafce66c323a3144d470547a71c5384c57623308ac8bb5e4b903ac148/yarl-1.24.5-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:24e861e9630e0daddcb9191fb187f60f034e17a4426f8101279f0c475cd74144", size = 115435, upload-time = "2026-07-20T02:06:24.923Z" }, + { url = "https://files.pythonhosted.org/packages/cf/f3/9b9d0e6d84bea851eb1ba99e4bdc755b86fd813e49ec86dfe42f26befdef/yarl-1.24.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9335a099ad87287c37fe5d1a982ff392fa5efe5d14b40a730b1ec1d6a41382b4", size = 110691, upload-time = "2026-07-20T02:06:26.973Z" }, + { url = "https://files.pythonhosted.org/packages/86/e4/62a06b7e87c4246ac76b7c2da136f972eb4a3a1fc94abb07e7022d6fdb0a/yarl-1.24.5-cp313-cp313-win_amd64.whl", hash = "sha256:2dbe06fc16bc91502bca713704022182e5729861ae00277c3a23354b40929740", size = 97454, upload-time = "2026-07-20T02:06:29.163Z" }, + { url = "https://files.pythonhosted.org/packages/9e/c9/5fc8025b318ab10db413b61056bd0d95c557a70e8df4210c7511f866329c/yarl-1.24.5-cp313-cp313-win_arm64.whl", hash = "sha256:6b8536851f9f65e7f00c7a1d49ba7f2be0ffe2c11555367fc9f50d9f842410a1", size = 92813, upload-time = "2026-07-20T02:06:31.113Z" }, + { url = "https://files.pythonhosted.org/packages/a9/08/5f3085fef9564217074db9dd8573de1795bc82cde61a7ad10b6a7234a569/yarl-1.24.5-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:2729fcfc4f6a596fb0c50f32090400aa9367774ac296a00387e65098c0befa76", size = 135680, upload-time = "2026-07-20T02:06:33.273Z" }, + { url = "https://files.pythonhosted.org/packages/98/35/ba9436e579bd48a8801f2021d842d9ab4994c26e4c7dd3a4c1f1bcb57a9e/yarl-1.24.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ff330d3c30db4eb6b01d79e29d2d0b407a7ecad39cfd9ec993ece57396a2ec0d", size = 97395, upload-time = "2026-07-20T02:06:35.259Z" }, + { url = "https://files.pythonhosted.org/packages/18/a9/a07f76f3c44e02b25cc743af5ef93eef27f7013eadca770451b6a6ccb5db/yarl-1.24.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e42d75862735da90e7fc5a7b23db0c976f737113a54b3c9777a9b665e9cbff75", size = 97223, upload-time = "2026-07-20T02:06:37.216Z" }, + { url = "https://files.pythonhosted.org/packages/77/f7/a9a1d6fa7dd9e388f95b30f6ad3ec4e285f6c8f61f44ce16070c3fcfe414/yarl-1.24.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a3732e66413163e72508da9eff9ce9d2846fde51fae45d3605393d3e6cd303e9", size = 108777, upload-time = "2026-07-20T02:06:39.292Z" }, + { url = "https://files.pythonhosted.org/packages/2f/44/e0b86c302471fabd6f02808ecf2ac52b8412b624787849d4bf2cdb466f6f/yarl-1.24.5-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5b8ee53be440a0cffc991a27be3057e0530122548dbe7c0892df08822fce5ede", size = 103119, upload-time = "2026-07-20T02:06:41.456Z" }, + { url = "https://files.pythonhosted.org/packages/d1/16/9c16d180bf8faaf223225eb50e1245870ff1ae0e302a27153988e65c51fd/yarl-1.24.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:af3aefa655adb5869491fa907e652290386800ae99cc50095cba71e2c6aefdca", size = 116471, upload-time = "2026-07-20T02:06:43.696Z" }, + { url = "https://files.pythonhosted.org/packages/d2/8d/b219b9df28a02ce95cfbdd41d2f7caa5669d0ff979c1c9975697145e33c5/yarl-1.24.5-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2120b96872df4a117cde97d270bac96aea7cc52205d305cf4611df694a487027", size = 115974, upload-time = "2026-07-20T02:06:45.874Z" }, + { url = "https://files.pythonhosted.org/packages/9b/e8/f20557aca240d88e69850ad1ee91756821d094bb1310565c04d25c6682a2/yarl-1.24.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:66410eb6345d467151934b49bfa70fb32f5b35a6140baa40ad97d6436abea2e9", size = 110830, upload-time = "2026-07-20T02:06:47.852Z" }, + { url = "https://files.pythonhosted.org/packages/db/18/199b85109a53eeca64ee19c9cca228287e8e4ab0cc1a09b28f530e65cce0/yarl-1.24.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4af7b7e1be0a69bee8210735fe6dcfc38879adfac6d62e789d53ba432d1ffa41", size = 110054, upload-time = "2026-07-20T02:06:49.84Z" }, + { url = "https://files.pythonhosted.org/packages/aa/2f/ed28147f8cd7f48c49367c90713b30a555284b6105a6a56f3a05568da795/yarl-1.24.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa139875ff98ab97da323cfadfaff08900d1ad42f1b5087b0b812a55c5a06373", size = 108312, upload-time = "2026-07-20T02:06:51.835Z" }, + { url = "https://files.pythonhosted.org/packages/c5/c5/55e16ae0a5c227cea8df1c6871ba57d614a34243146c05729caf2a1bd9c5/yarl-1.24.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:0055afc45e864b92729ac7600e2d102c17bef060647e74bca75fa84d66b9ff36", size = 103662, upload-time = "2026-07-20T02:06:54.061Z" }, + { url = "https://files.pythonhosted.org/packages/8d/ea/dbd7c2caec459c9a426f18b02688ecbfb58620d0f6a3422d24769fbaf8ab/yarl-1.24.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f0e466ed7511fe9d459a819edbc6c2585c0b6eabde9fa8a8947552468a7a6ef0", size = 116090, upload-time = "2026-07-20T02:06:56.015Z" }, + { url = "https://files.pythonhosted.org/packages/06/84/39ce4ce3059e07fece5fbdbee8c4053406af9aca911ce9fa5f8548aab6af/yarl-1.24.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:f141474e85b7e54998ec5180530a7cda99ab29e282fa50e0756d89981a9b43c5", size = 109523, upload-time = "2026-07-20T02:06:57.926Z" }, + { url = "https://files.pythonhosted.org/packages/a9/8b/71ff44137b405c64a7788075669c24010019f57a7464b78c3a6cbee539d9/yarl-1.24.5-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:e2935f8c39e3b03e83519292d78f075189978f3f4adc15a78144c7c8e2a1cba5", size = 116084, upload-time = "2026-07-20T02:06:59.868Z" }, + { url = "https://files.pythonhosted.org/packages/62/c0/423078fdd4042e1862c11f0ffd977a0ffa393783c12bee94685923bc189e/yarl-1.24.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9d1216a7f6f77836617dba35687c5b78a4170afc3c3f18fc788f785ba26565c4", size = 111006, upload-time = "2026-07-20T02:07:01.907Z" }, + { url = "https://files.pythonhosted.org/packages/cf/52/6daa2ee9d95e5c98b8128f8df91eb692eb423ab274b8cf08db52152fad26/yarl-1.24.5-cp314-cp314-win_amd64.whl", hash = "sha256:5ba4f78df2bcc19f764a4b26a8a4f5049c110090ad5825993aacb052bf8003ad", size = 99215, upload-time = "2026-07-20T02:07:03.852Z" }, + { url = "https://files.pythonhosted.org/packages/ec/0e/464a847d7359e0da75dd9fc5c1d1aa35d0159ea31e5f8e66a3c1c29ff3d0/yarl-1.24.5-cp314-cp314-win_arm64.whl", hash = "sha256:9e4e16c73d717c5cf27626c524d0a2e261ad20e46932b2670f64ad5dde23e26f", size = 94566, upload-time = "2026-07-20T02:07:06.074Z" }, + { url = "https://files.pythonhosted.org/packages/e2/55/e03acc4446772660bc335e86e41ef31e4d0d838fd641531a11a5ee33b493/yarl-1.24.5-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:e1ae548a9d901adca07899a4147a7c826bbcc06239d3ce9a59f57886a28a4c88", size = 142533, upload-time = "2026-07-20T02:07:08.284Z" }, + { url = "https://files.pythonhosted.org/packages/ae/71/4acd3a1fc7cf14345cdb302665ecd2097f62c365b4f14ca17d4f37775cf9/yarl-1.24.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:ff405d91509d88e8d44129cd87b18d70acd1f0c1aeabd7bc3c46792b1fe2acba", size = 100776, upload-time = "2026-07-20T02:07:10.197Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0b/cfb76b7fe99686db264bff829779a539d923e7564ffd7ef18da6c54c3774/yarl-1.24.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:47e98aab9d8d82ff682e7b0b5dded33bf138a32b817fcf7fa3b27b2d7c412928", size = 100913, upload-time = "2026-07-20T02:07:12.357Z" }, + { url = "https://files.pythonhosted.org/packages/8b/3f/7116e782992abbd4fb6948488aec72078895e929a23078290739e8396fce/yarl-1.24.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f0a658a6d3fafee5c6f63c58f3e785c8c43c93fbc02bf9f2b6663f8185e0971f", size = 106507, upload-time = "2026-07-20T02:07:14.173Z" }, + { url = "https://files.pythonhosted.org/packages/33/90/d4d2d73ee78229cc889872eb8e085d8f5c6f51abdb178409fd9b23cf74fd/yarl-1.24.5-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4377407001ca3c057773f44d8ddd6358fa5f691407c1ba92210bd3cf8d9e4c95", size = 99219, upload-time = "2026-07-20T02:07:16.019Z" }, + { url = "https://files.pythonhosted.org/packages/3e/fa/a6df1a9bccd644eec00abee0dff4277416222cec435330fd1f2858523ec1/yarl-1.24.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7c0494a31a1ac5461a226e7947a9c9b78c44e1dc7185164fa7e9651557a5d9bc", size = 111804, upload-time = "2026-07-20T02:07:18.141Z" }, + { url = "https://files.pythonhosted.org/packages/8a/9e/7b2a1f4bcc20e9447156dd2b1c4d01f70d9df0759025ee7d09a84ffae134/yarl-1.24.5-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a7cff474ab7cd149765bb784cf6d78b32e18e20473fb7bda860bce98ab58e9da", size = 110943, upload-time = "2026-07-20T02:07:20.06Z" }, + { url = "https://files.pythonhosted.org/packages/08/ff/22c92affb0f9b623ca753d27d968b5625b868f12c6378d049d55ae247643/yarl-1.24.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cbb833ccacdb5519eff9b8b71ee618cc2801c878e77e288775d77c3a2ced858a", size = 108251, upload-time = "2026-07-20T02:07:22.217Z" }, + { url = "https://files.pythonhosted.org/packages/45/44/5769b96298c1e195fb412997b6090af2a84105cf59c17613558a2d011d1f/yarl-1.24.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:82f75e05912e84b7a0fe57075d9c59de3cb352b928330f2eb69b2e1f54c3e1f0", size = 106025, upload-time = "2026-07-20T02:07:24.083Z" }, + { url = "https://files.pythonhosted.org/packages/4c/40/009e8e791fd9762c0e1567e69248acb4f49064597e1680874c16dd8bb798/yarl-1.24.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:16a2f5010280020e90f5330257e6944bc33e73593b136cc5a241e6c1dc292498", size = 106573, upload-time = "2026-07-20T02:07:26.248Z" }, + { url = "https://files.pythonhosted.org/packages/20/c6/b7480578f8a0a80946f36ad6df547ecec704f9ba69d2de60f8aa6f1c1cbf/yarl-1.24.5-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:ffcd54362564dc1a30fb74d8b8a6e5a6b11ebd5e27266adc3b7427a21a6c9104", size = 100751, upload-time = "2026-07-20T02:07:28.098Z" }, + { url = "https://files.pythonhosted.org/packages/d4/27/4476f3360b91a48c5cf125e91f59a3bd35299d84a431a258d57f5977bb11/yarl-1.24.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0465ec8cedc2349b97a6b595ace64084a50c6e839eca40aa0626f38b8350e331", size = 111643, upload-time = "2026-07-20T02:07:30.88Z" }, + { url = "https://files.pythonhosted.org/packages/4c/4b/5cdd3e5ee944e8af31e52f6cd3d3af5fd7b937e036ccbbba2c9ffebede95/yarl-1.24.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:4db9aecb141cb7a5447171b57aa1ed3a8fee06af40b992ffc31206c0b0121550", size = 106312, upload-time = "2026-07-20T02:07:33.06Z" }, + { url = "https://files.pythonhosted.org/packages/18/86/f406b0c2a6f99575de2da671ef47aa06f89a5be83a27a46971c3b86cecdb/yarl-1.24.5-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:f540c013589084679a6c7fac07096b10159737918174f5dfc5e11bf5bca4dfe6", size = 110379, upload-time = "2026-07-20T02:07:35.155Z" }, + { url = "https://files.pythonhosted.org/packages/f0/6c/9f3adfbd3b30b4fa0f7ccb3a83eba2c1152d3fff554d535e640ba0f7ba2b/yarl-1.24.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a61834fb15d81322d872eaafd333838ae7c9cea84067f232656f75965933d047", size = 108497, upload-time = "2026-07-20T02:07:37.35Z" }, + { url = "https://files.pythonhosted.org/packages/dd/37/91eb2e5ca883a529c1b390348a74cd9fc0512171727f547ce70bfe02be5c/yarl-1.24.5-cp314-cp314t-win_amd64.whl", hash = "sha256:5c88e5815a49d289e599f3513aa7fde0bc2092ff188f99c940f007f90f53d104", size = 102450, upload-time = "2026-07-20T02:07:39.578Z" }, + { url = "https://files.pythonhosted.org/packages/bf/f4/ed5c402ac8fde4403ed3366c2716bfddc8a6677ebd59f3d62772cc7fe468/yarl-1.24.5-cp314-cp314t-win_arm64.whl", hash = "sha256:cf139c02f5f23ef6532040a30ff662c00a318c952334f211046b8e60b7f17688", size = 97222, upload-time = "2026-07-20T02:07:41.55Z" }, + { url = "https://files.pythonhosted.org/packages/61/02/962c1cbfc401a30c1d034dc67ff395f64b52302c6d62de556c1fca99acc0/yarl-1.24.5-py3-none-any.whl", hash = "sha256:a33700d13d9b7d84fd10947b09ff69fb9a792e519c8cb9764a3ca70baa6c23a7", size = 58612, upload-time = "2026-07-20T02:07:43.461Z" }, +]