From 516c2ed136b070242c34f56b49e70256e58861ac Mon Sep 17 00:00:00 2001 From: Wu Clan Date: Thu, 6 Nov 2025 21:30:08 +0800 Subject: [PATCH 01/25] Refactor foreign keys and relationships to pure logic --- backend/app/admin/crud/crud_user.py | 80 ++- backend/app/admin/model/data_rule.py | 13 +- backend/app/admin/model/data_scope.py | 16 +- backend/app/admin/model/dept.py | 20 +- backend/app/admin/model/m2m.py | 56 +- backend/app/admin/model/menu.py | 21 +- backend/app/admin/model/role.py | 19 +- backend/app/admin/model/user.py | 19 +- backend/app/admin/service/user_service.py | 6 +- backend/common/security/jwt.py | 5 +- .../plugin/code_generator/model/business.py | 11 +- backend/plugin/code_generator/model/column.py | 16 +- backend/plugin/dict/model/dict_data.py | 16 +- backend/plugin/dict/model/dict_type.py | 12 +- backend/plugin/oauth2/model/user_social.py | 16 +- backend/utils/serializers.py | 271 ++++++++- requirements.txt | 16 +- uv.lock | 520 +++++++++--------- 18 files changed, 644 insertions(+), 489 deletions(-) diff --git a/backend/app/admin/crud/crud_user.py b/backend/app/admin/crud/crud_user.py index d6155ce9..92df7c9f 100644 --- a/backend/app/admin/crud/crud_user.py +++ b/backend/app/admin/crud/crud_user.py @@ -1,18 +1,21 @@ +from typing import Any + import bcrypt -from sqlalchemy import select +from sqlalchemy import delete, insert, select from sqlalchemy.ext.asyncio import AsyncSession -from sqlalchemy.orm import noload, selectinload from sqlalchemy.sql import Select -from sqlalchemy_crud_plus import CRUDPlus +from sqlalchemy_crud_plus import CRUDPlus, JoinConfig -from backend.app.admin.model import Dept, Role, User +from backend.app.admin.model import DataRule, DataScope, Dept, Menu, Role, User +from backend.app.admin.model.m2m import data_scope_rule, role_data_scope, role_menu, user_role from backend.app.admin.schema.user import ( AddOAuth2UserParam, AddUserParam, UpdateUserParam, ) from backend.common.security.jwt import get_hash_password +from backend.utils.serializers import select_join_serialize from backend.utils.timezone import timezone @@ -73,11 +76,13 @@ async def add(self, db: AsyncSession, obj: AddUserParam) -> None: dict_obj.update({'salt': salt}) new_user = self.model(**dict_obj) - stmt = select(Role).where(Role.id.in_(obj.roles)) - roles = await db.execute(stmt) - new_user.roles = roles.scalars().all() - db.add(new_user) + await db.flush() # 获取用户ID + + # 添加用户角色关联(逻辑外键) + if obj.roles: + for role_id in obj.roles: + await db.execute(insert(user_role).values(user_id=new_user.id, role_id=role_id)) async def add_by_oauth2(self, db: AsyncSession, obj: AddOAuth2UserParam) -> None: """ @@ -91,11 +96,15 @@ async def add_by_oauth2(self, db: AsyncSession, obj: AddOAuth2UserParam) -> None dict_obj.update({'is_staff': True, 'salt': None}) new_user = self.model(**dict_obj) - stmt = select(Role) - role = await db.execute(stmt) - new_user.roles = [role.scalars().first()] # 默认绑定第一个角色 - db.add(new_user) + await db.flush() # 获取用户ID + + # 绑定第一个角色(逻辑外键) + stmt = select(Role).limit(1) + role = await db.execute(stmt) + first_role = role.scalars().first() + if first_role: + await db.execute(insert(user_role).values(user_id=new_user.id, role_id=first_role.id)) async def update(self, db: AsyncSession, input_user: User, obj: UpdateUserParam) -> int: """ @@ -111,9 +120,14 @@ async def update(self, db: AsyncSession, input_user: User, obj: UpdateUserParam) count = await self.update_model(db, input_user.id, obj) - stmt = select(Role).where(Role.id.in_(role_ids)) - roles = await db.execute(stmt) - input_user.roles = roles.scalars().all() + # 删除原有用户角色关联 + await db.execute(delete(user_role).where(user_role.c.user_id == input_user.id)) + + # 添加新的用户角色关联(逻辑外键) + if role_ids: + for role_id in role_ids: + await db.execute(insert(user_role).values(user_id=input_user.id, role_id=role_id)) + return count async def update_nickname(self, db: AsyncSession, user_id: int, nickname: str) -> int: @@ -206,10 +220,6 @@ async def get_select(self, dept: int | None, username: str | None, phone: str | return await self.select_order( 'id', 'desc', - load_options=[ - selectinload(self.model.dept).options(noload(Dept.parent), noload(Dept.children), noload(Dept.users)), - selectinload(self.model.roles).options(noload(Role.users), noload(Role.menus), noload(Role.scopes)), - ], **filters, ) @@ -257,20 +267,20 @@ async def set_multi_login(self, db: AsyncSession, user_id: int, *, multi_login: """ return await self.update_model(db, user_id, {'is_multi_login': multi_login}) - async def get_with_relation( + async def get_joins( self, db: AsyncSession, *, user_id: int | None = None, username: str | None = None, - ) -> User | None: + ) -> Any | None: """ 获取用户关联信息 :param db: 数据库会话 :param user_id: 用户 ID :param username: 用户名 - :return: + :return: 包含用户信息及关联部门、角色等数据的对象,支持访问 .status、.dept、.roles 等属性 """ filters = {} @@ -279,12 +289,32 @@ async def get_with_relation( if username: filters['username'] = username - return await self.select_model_by_column( + result = await self.select_models( db, - load_options=[selectinload(self.model.roles).options(selectinload(Role.menus), selectinload(Role.scopes))], - load_strategies=['dept'], + join_conditions=[ + JoinConfig(model=Dept, join_on=Dept.id == self.model.dept_id, fill_result=True), + JoinConfig(model=user_role, join_on=user_role.c.user_id == self.model.id), + JoinConfig(model=Role, join_on=Role.id == user_role.c.role_id, fill_result=True), + JoinConfig(model=role_menu, join_on=role_menu.c.role_id == Role.id), + JoinConfig(model=Menu, join_on=Menu.id == role_menu.c.menu_id, fill_result=True), + JoinConfig(model=role_data_scope, join_on=role_data_scope.c.role_id == Role.id), + JoinConfig(model=DataScope, join_on=DataScope.id == role_data_scope.c.data_scope_id, fill_result=True), + JoinConfig(model=data_scope_rule, join_on=data_scope_rule.c.data_scope_id == DataScope.id), + JoinConfig(model=DataRule, join_on=DataRule.id == data_scope_rule.c.data_rule_id, fill_result=True), + ], **filters, ) + return select_join_serialize( + result, + relationships=[ + 'User-m2o-Dept', + 'User-m2m-Role', + 'Role-m2m-Menu', + 'Role-m2m-DataScope', + 'DataScope-m2m-DataRule', + ], + ) + user_dao: CRUDUser = CRUDUser(User) diff --git a/backend/app/admin/model/data_rule.py b/backend/app/admin/model/data_rule.py index 0729320a..e3db5638 100644 --- a/backend/app/admin/model/data_rule.py +++ b/backend/app/admin/model/data_rule.py @@ -1,17 +1,9 @@ -from __future__ import annotations - -from typing import TYPE_CHECKING - import sqlalchemy as sa -from sqlalchemy.orm import Mapped, mapped_column, relationship +from sqlalchemy.orm import Mapped, mapped_column -from backend.app.admin.model.m2m import sys_data_scope_rule from backend.common.model import Base, id_key -if TYPE_CHECKING: - from backend.app.admin.model import DataScope - class DataRule(Base): """数据规则表""" @@ -27,6 +19,3 @@ class DataRule(Base): comment='表达式(0:==、1:!=、2:>、3:>=、4:<、5:<=、6:in、7:not_in)', ) value: Mapped[str] = mapped_column(sa.String(256), comment='规则值') - - # 数据范围规则多对多 - scopes: Mapped[list[DataScope]] = relationship(init=False, secondary=sys_data_scope_rule, back_populates='rules') diff --git a/backend/app/admin/model/data_scope.py b/backend/app/admin/model/data_scope.py index 41eb0f4c..ae16394c 100644 --- a/backend/app/admin/model/data_scope.py +++ b/backend/app/admin/model/data_scope.py @@ -1,17 +1,9 @@ -from __future__ import annotations - -from typing import TYPE_CHECKING - import sqlalchemy as sa -from sqlalchemy.orm import Mapped, mapped_column, relationship +from sqlalchemy.orm import Mapped, mapped_column -from backend.app.admin.model.m2m import sys_data_scope_rule, sys_role_data_scope from backend.common.model import Base, id_key -if TYPE_CHECKING: - from backend.app.admin.model import DataRule, Role - class DataScope(Base): """数据范围表""" @@ -21,9 +13,3 @@ class DataScope(Base): id: Mapped[id_key] = mapped_column(init=False) name: Mapped[str] = mapped_column(sa.String(64), unique=True, comment='名称') status: Mapped[int] = mapped_column(default=1, comment='状态(0停用 1正常)') - - # 数据范围规则多对多 - rules: Mapped[list[DataRule]] = relationship(init=False, secondary=sys_data_scope_rule, back_populates='scopes') - - # 角色数据范围多对多 - roles: Mapped[list[Role]] = relationship(init=False, secondary=sys_role_data_scope, back_populates='scopes') diff --git a/backend/app/admin/model/dept.py b/backend/app/admin/model/dept.py index 1c15f286..7b5a6a19 100644 --- a/backend/app/admin/model/dept.py +++ b/backend/app/admin/model/dept.py @@ -1,16 +1,9 @@ -from __future__ import annotations - -from typing import TYPE_CHECKING - import sqlalchemy as sa -from sqlalchemy.orm import Mapped, mapped_column, relationship +from sqlalchemy.orm import Mapped, mapped_column from backend.common.model import Base, id_key -if TYPE_CHECKING: - from backend.app.admin.model import User - class Dept(Base): """部门表""" @@ -26,12 +19,5 @@ class Dept(Base): status: Mapped[int] = mapped_column(default=1, comment='部门状态(0停用 1正常)') del_flag: Mapped[bool] = mapped_column(default=False, comment='删除标志(0删除 1存在)') - # 父级部门一对多 - parent_id: Mapped[int | None] = mapped_column( - sa.BigInteger, sa.ForeignKey('sys_dept.id', ondelete='SET NULL'), default=None, index=True, comment='父部门ID' - ) - parent: Mapped[Dept | None] = relationship(init=False, back_populates='children', remote_side=[id]) - children: Mapped[list[Dept] | None] = relationship(init=False, back_populates='parent') - - # 部门用户一对多 - users: Mapped[list[User]] = relationship(init=False, back_populates='dept') + # 父级部门 + parent_id: Mapped[int | None] = mapped_column(sa.BigInteger, default=None, index=True, comment='父部门ID') diff --git a/backend/app/admin/model/m2m.py b/backend/app/admin/model/m2m.py index 310d3a8b..330975b3 100644 --- a/backend/app/admin/model/m2m.py +++ b/backend/app/admin/model/m2m.py @@ -2,62 +2,38 @@ from backend.common.model import MappedBase -sys_user_role = sa.Table( +# 用户角色表 +user_role = sa.Table( 'sys_user_role', MappedBase.metadata, sa.Column('id', sa.BigInteger, primary_key=True, unique=True, index=True, autoincrement=True, comment='主键ID'), - sa.Column( - 'user_id', sa.BigInteger, sa.ForeignKey('sys_user.id', ondelete='CASCADE'), primary_key=True, comment='用户ID' - ), - sa.Column( - 'role_id', sa.BigInteger, sa.ForeignKey('sys_role.id', ondelete='CASCADE'), primary_key=True, comment='角色ID' - ), + sa.Column('user_id', sa.BigInteger, primary_key=True, comment='用户ID'), + sa.Column('role_id', sa.BigInteger, primary_key=True, comment='角色ID'), ) -sys_role_menu = sa.Table( +# 角色菜单表 +role_menu = sa.Table( 'sys_role_menu', MappedBase.metadata, sa.Column('id', sa.BigInteger, primary_key=True, unique=True, index=True, autoincrement=True, comment='主键ID'), - sa.Column( - 'role_id', sa.BigInteger, sa.ForeignKey('sys_role.id', ondelete='CASCADE'), primary_key=True, comment='角色ID' - ), - sa.Column( - 'menu_id', sa.BigInteger, sa.ForeignKey('sys_menu.id', ondelete='CASCADE'), primary_key=True, comment='菜单ID' - ), + sa.Column('role_id', sa.BigInteger, primary_key=True, comment='角色ID'), + sa.Column('menu_id', sa.BigInteger, primary_key=True, comment='菜单ID'), ) -sys_role_data_scope = sa.Table( +# 角色数据范围表 +role_data_scope = sa.Table( 'sys_role_data_scope', MappedBase.metadata, sa.Column('id', sa.BigInteger, primary_key=True, unique=True, index=True, autoincrement=True, comment='主键 ID'), - sa.Column( - 'role_id', sa.BigInteger, sa.ForeignKey('sys_role.id', ondelete='CASCADE'), primary_key=True, comment='角色 ID' - ), - sa.Column( - 'data_scope_id', - sa.BigInteger, - sa.ForeignKey('sys_data_scope.id', ondelete='CASCADE'), - primary_key=True, - comment='数据范围 ID', - ), + sa.Column('role_id', sa.BigInteger, primary_key=True, comment='角色 ID'), + sa.Column('data_scope_id', sa.BigInteger, primary_key=True, comment='数据范围 ID'), ) -sys_data_scope_rule = sa.Table( +# 数据范围规则表 +data_scope_rule = sa.Table( 'sys_data_scope_rule', MappedBase.metadata, sa.Column('id', sa.BigInteger, primary_key=True, unique=True, index=True, autoincrement=True, comment='主键ID'), - sa.Column( - 'data_scope_id', - sa.BigInteger, - sa.ForeignKey('sys_data_scope.id', ondelete='CASCADE'), - primary_key=True, - comment='数据范围 ID', - ), - sa.Column( - 'data_rule_id', - sa.BigInteger, - sa.ForeignKey('sys_data_rule.id', ondelete='CASCADE'), - primary_key=True, - comment='数据规则 ID', - ), + sa.Column('data_scope_id', sa.BigInteger, primary_key=True, comment='数据范围 ID'), + sa.Column('data_rule_id', sa.BigInteger, primary_key=True, comment='数据规则 ID'), ) diff --git a/backend/app/admin/model/menu.py b/backend/app/admin/model/menu.py index d4adc594..0e72d15f 100644 --- a/backend/app/admin/model/menu.py +++ b/backend/app/admin/model/menu.py @@ -1,17 +1,9 @@ -from __future__ import annotations - -from typing import TYPE_CHECKING - import sqlalchemy as sa -from sqlalchemy.orm import Mapped, mapped_column, relationship +from sqlalchemy.orm import Mapped, mapped_column -from backend.app.admin.model.m2m import sys_role_menu from backend.common.model import Base, UniversalText, id_key -if TYPE_CHECKING: - from backend.app.admin.model import Role - class Menu(Base): """菜单表""" @@ -33,12 +25,5 @@ class Menu(Base): link: Mapped[str | None] = mapped_column(UniversalText, default=None, comment='外链地址') remark: Mapped[str | None] = mapped_column(UniversalText, default=None, comment='备注') - # 父级菜单一对多 - parent_id: Mapped[int | None] = mapped_column( - sa.BigInteger, sa.ForeignKey('sys_menu.id', ondelete='SET NULL'), default=None, index=True, comment='父菜单ID' - ) - parent: Mapped[Menu | None] = relationship(init=False, back_populates='children', remote_side=[id]) - children: Mapped[list[Menu] | None] = relationship(init=False, back_populates='parent') - - # 菜单角色多对多 - roles: Mapped[list[Role]] = relationship(init=False, secondary=sys_role_menu, back_populates='menus') + # 父级菜单 + parent_id: Mapped[int | None] = mapped_column(sa.BigInteger, default=None, index=True, comment='父菜单ID') diff --git a/backend/app/admin/model/role.py b/backend/app/admin/model/role.py index 3517bbfe..ff1edeb7 100644 --- a/backend/app/admin/model/role.py +++ b/backend/app/admin/model/role.py @@ -1,17 +1,9 @@ -from __future__ import annotations - -from typing import TYPE_CHECKING - import sqlalchemy as sa -from sqlalchemy.orm import Mapped, mapped_column, relationship +from sqlalchemy.orm import Mapped, mapped_column -from backend.app.admin.model.m2m import sys_role_data_scope, sys_role_menu, sys_user_role from backend.common.model import Base, UniversalText, id_key -if TYPE_CHECKING: - from backend.app.admin.model import DataScope, Menu, User - class Role(Base): """角色表""" @@ -23,12 +15,3 @@ class Role(Base): status: Mapped[int] = mapped_column(default=1, comment='角色状态(0停用 1正常)') is_filter_scopes: Mapped[bool] = mapped_column(default=True, comment='过滤数据权限(0否 1是)') remark: Mapped[str | None] = mapped_column(UniversalText, default=None, comment='备注') - - # 角色用户多对多 - users: Mapped[list[User]] = relationship(init=False, secondary=sys_user_role, back_populates='roles') - - # 角色菜单多对多 - menus: Mapped[list[Menu]] = relationship(init=False, secondary=sys_role_menu, back_populates='roles') - - # 角色数据范围多对多 - scopes: Mapped[list[DataScope]] = relationship(init=False, secondary=sys_role_data_scope, back_populates='roles') diff --git a/backend/app/admin/model/user.py b/backend/app/admin/model/user.py index 363c3bc4..52012193 100644 --- a/backend/app/admin/model/user.py +++ b/backend/app/admin/model/user.py @@ -1,20 +1,13 @@ -from __future__ import annotations - from datetime import datetime -from typing import TYPE_CHECKING import sqlalchemy as sa -from sqlalchemy.orm import Mapped, mapped_column, relationship +from sqlalchemy.orm import Mapped, mapped_column -from backend.app.admin.model.m2m import sys_user_role from backend.common.model import Base, TimeZone, id_key from backend.database.db import uuid4_str from backend.utils.timezone import timezone -if TYPE_CHECKING: - from backend.app.admin.model import Dept, Role - class User(Base): """用户表""" @@ -39,11 +32,5 @@ class User(Base): TimeZone, init=False, onupdate=timezone.now, comment='上次登录' ) - # 部门用户一对多 - dept_id: Mapped[int | None] = mapped_column( - sa.BigInteger, sa.ForeignKey('sys_dept.id', ondelete='SET NULL'), default=None, comment='部门关联ID' - ) - dept: Mapped[Dept | None] = relationship(init=False, back_populates='users') - - # 用户角色多对多 - roles: Mapped[list[Role]] = relationship(init=False, secondary=sys_user_role, back_populates='users') + # 逻辑外键 + dept_id: Mapped[int | None] = mapped_column(sa.BigInteger, default=None, comment='部门关联ID') diff --git a/backend/app/admin/service/user_service.py b/backend/app/admin/service/user_service.py index f214220e..c5eb1eb4 100644 --- a/backend/app/admin/service/user_service.py +++ b/backend/app/admin/service/user_service.py @@ -38,7 +38,7 @@ async def get_userinfo(*, db: AsyncSession, pk: int | None = None, username: str :param username: 用户名 :return: """ - user = await user_dao.get_with_relation(db, user_id=pk, username=username) + user = await user_dao.get_joins(db, user_id=pk, username=username) if not user: raise errors.NotFoundError(msg='用户不存在') return user @@ -52,7 +52,7 @@ async def get_roles(*, db: AsyncSession, pk: int) -> Sequence[Role]: :param pk: 用户 ID :return: """ - user = await user_dao.get_with_relation(db, user_id=pk) + user = await user_dao.get_joins(db, user_id=pk) if not user: raise errors.NotFoundError(msg='用户不存在') return user.roles @@ -103,7 +103,7 @@ async def update(*, db: AsyncSession, pk: int, obj: UpdateUserParam) -> int: :param obj: 用户更新参数 :return: """ - user = await user_dao.get_with_relation(db, user_id=pk) + user = await user_dao.get_joins(db, user_id=pk) if not user: raise errors.NotFoundError(msg='用户不存在') if obj.username != user.username and await user_dao.get_by_username(db, obj.username): diff --git a/backend/common/security/jwt.py b/backend/common/security/jwt.py index 96bb4e97..fc8e0b42 100644 --- a/backend/common/security/jwt.py +++ b/backend/common/security/jwt.py @@ -22,7 +22,6 @@ from backend.core.conf import settings from backend.database.db import async_db_session from backend.database.redis import redis_client -from backend.utils.serializers import select_as_dict from backend.utils.timezone import timezone @@ -246,7 +245,7 @@ async def get_current_user(db: AsyncSession, pk: int) -> User: """ from backend.app.admin.crud.crud_user import user_dao - user = await user_dao.get_with_relation(db, user_id=pk) + user = await user_dao.get_joins(db, user_id=pk) if not user: raise errors.TokenError(msg='Token 无效') if not user.status: @@ -297,7 +296,7 @@ async def jwt_authentication(token: str) -> GetUserInfoWithRelationDetail: if not cache_user: async with async_db_session() as db: current_user = await get_current_user(db, user_id) - user = GetUserInfoWithRelationDetail(**select_as_dict(current_user)) + user = GetUserInfoWithRelationDetail.model_validate(current_user) await redis_client.setex( f'{settings.JWT_USER_REDIS_PREFIX}:{user_id}', settings.TOKEN_EXPIRE_SECONDS, diff --git a/backend/plugin/code_generator/model/business.py b/backend/plugin/code_generator/model/business.py index b791d273..3e560e8b 100644 --- a/backend/plugin/code_generator/model/business.py +++ b/backend/plugin/code_generator/model/business.py @@ -1,16 +1,9 @@ -from __future__ import annotations - -from typing import TYPE_CHECKING - import sqlalchemy as sa -from sqlalchemy.orm import Mapped, mapped_column, relationship +from sqlalchemy.orm import Mapped, mapped_column from backend.common.model import Base, UniversalText, id_key -if TYPE_CHECKING: - from backend.plugin.code_generator.model import GenColumn - class GenBusiness(Base): """代码生成业务表""" @@ -34,5 +27,3 @@ class GenBusiness(Base): sa.String(256), default=None, comment='代码生成路径(默认为 app 根路径)' ) remark: Mapped[str | None] = mapped_column(UniversalText, default=None, comment='备注') - # 代码生成业务模型列一对多 - gen_column: Mapped[list[GenColumn]] = relationship(init=False, back_populates='gen_business') diff --git a/backend/plugin/code_generator/model/column.py b/backend/plugin/code_generator/model/column.py index e3f0cb28..00718be2 100644 --- a/backend/plugin/code_generator/model/column.py +++ b/backend/plugin/code_generator/model/column.py @@ -1,16 +1,9 @@ -from __future__ import annotations - -from typing import TYPE_CHECKING - import sqlalchemy as sa -from sqlalchemy.orm import Mapped, mapped_column, relationship +from sqlalchemy.orm import Mapped, mapped_column from backend.common.model import DataClassBase, UniversalText, id_key -if TYPE_CHECKING: - from backend.plugin.code_generator.model import GenBusiness - class GenColumn(DataClassBase): """代码生成模型列表""" @@ -28,8 +21,5 @@ class GenColumn(DataClassBase): is_pk: Mapped[bool] = mapped_column(default=False, comment='是否主键') is_nullable: Mapped[bool] = mapped_column(default=False, comment='是否可为空') - # 代码生成业务模型列一对多 - gen_business_id: Mapped[int] = mapped_column( - sa.BigInteger, sa.ForeignKey('gen_business.id', ondelete='CASCADE'), default=0, comment='代码生成业务ID' - ) - gen_business: Mapped[GenBusiness | None] = relationship(init=False, back_populates='gen_column') + # 逻辑外键 + gen_business_id: Mapped[int] = mapped_column(sa.BigInteger, default=0, comment='代码生成业务ID') diff --git a/backend/plugin/dict/model/dict_data.py b/backend/plugin/dict/model/dict_data.py index 22bda452..1ade4785 100644 --- a/backend/plugin/dict/model/dict_data.py +++ b/backend/plugin/dict/model/dict_data.py @@ -1,16 +1,9 @@ -from __future__ import annotations - -from typing import TYPE_CHECKING - import sqlalchemy as sa -from sqlalchemy.orm import Mapped, mapped_column, relationship +from sqlalchemy.orm import Mapped, mapped_column from backend.common.model import Base, UniversalText, id_key -if TYPE_CHECKING: - from backend.plugin.dict.model import DictType - class DictData(Base): """字典数据表""" @@ -26,8 +19,5 @@ class DictData(Base): status: Mapped[int] = mapped_column(default=1, comment='状态(0停用 1正常)') remark: Mapped[str | None] = mapped_column(UniversalText, default=None, comment='备注') - # 字典类型一对多 - type_id: Mapped[int] = mapped_column( - sa.ForeignKey('sys_dict_type.id', ondelete='CASCADE'), default=0, comment='字典类型关联ID' - ) - type: Mapped[DictType] = relationship(init=False, back_populates='datas') + # 逻辑外键 + type_id: Mapped[int] = mapped_column(sa.BigInteger, default=0, comment='字典类型关联ID') diff --git a/backend/plugin/dict/model/dict_type.py b/backend/plugin/dict/model/dict_type.py index 8fb2c4cc..41ee4440 100644 --- a/backend/plugin/dict/model/dict_type.py +++ b/backend/plugin/dict/model/dict_type.py @@ -1,16 +1,9 @@ -from __future__ import annotations - -from typing import TYPE_CHECKING - import sqlalchemy as sa -from sqlalchemy.orm import Mapped, mapped_column, relationship +from sqlalchemy.orm import Mapped, mapped_column from backend.common.model import Base, UniversalText, id_key -if TYPE_CHECKING: - from backend.plugin.dict.model import DictData - class DictType(Base): """字典类型表""" @@ -21,6 +14,3 @@ class DictType(Base): name: Mapped[str] = mapped_column(sa.String(32), comment='字典类型名称') code: Mapped[str] = mapped_column(sa.String(32), unique=True, comment='字典类型编码') remark: Mapped[str | None] = mapped_column(UniversalText, default=None, comment='备注') - - # 字典类型一对多 - datas: Mapped[list[DictData]] = relationship(init=False, back_populates='type') diff --git a/backend/plugin/oauth2/model/user_social.py b/backend/plugin/oauth2/model/user_social.py index 59e2515e..03a83339 100644 --- a/backend/plugin/oauth2/model/user_social.py +++ b/backend/plugin/oauth2/model/user_social.py @@ -1,16 +1,9 @@ -from __future__ import annotations - -from typing import TYPE_CHECKING - import sqlalchemy as sa -from sqlalchemy.orm import Mapped, mapped_column, relationship +from sqlalchemy.orm import Mapped, mapped_column from backend.common.model import Base, id_key -if TYPE_CHECKING: - from backend.app.admin.model import User - class UserSocial(Base): """用户社交表(OAuth2)""" @@ -21,8 +14,5 @@ class UserSocial(Base): sid: Mapped[str] = mapped_column(sa.String(256), comment='第三方用户 ID') source: Mapped[str] = mapped_column(sa.String(32), comment='第三方用户来源') - # 用户社交信息一对多 - user_id: Mapped[int] = mapped_column( - sa.BigInteger, sa.ForeignKey('sys_user.id', ondelete='CASCADE'), comment='用户关联ID' - ) - user: Mapped[User | None] = relationship(init=False, backref='socials') + # 逻辑外键 + user_id: Mapped[int] = mapped_column(sa.BigInteger, comment='用户关联ID') diff --git a/backend/utils/serializers.py b/backend/utils/serializers.py index 520e5974..ec85e364 100644 --- a/backend/utils/serializers.py +++ b/backend/utils/serializers.py @@ -1,5 +1,10 @@ +import operator + +from collections import defaultdict, namedtuple from collections.abc import Sequence from decimal import Decimal +from functools import lru_cache +from itertools import starmap from typing import Any, TypeVar from fastapi.encoders import decimal_encoder @@ -21,11 +26,16 @@ def select_columns_serialize(row: R) -> dict[str, Any]: :return: """ result = {} + + if not row: + return result + for column in row.__table__.columns.keys(): value = getattr(row, column) if isinstance(value, Decimal): value = decimal_encoder(value) result[column] = value + return result @@ -47,12 +57,16 @@ def select_as_dict(row: R, *, use_alias: bool = False) -> dict[str, Any]: :param use_alias: 是否使用别名作为列名 :return: """ + result = {} + + if not row: + return result + if not use_alias: result = row.__dict__ if '_sa_instance_state' in result: del result['_sa_instance_state'] else: - result = {} mapper = class_mapper(row.__class__) # type: ignore for prop in mapper.iterate_properties: if isinstance(prop, (ColumnProperty, SynonymProperty)): @@ -62,6 +76,261 @@ def select_as_dict(row: R, *, use_alias: bool = False) -> dict[str, Any]: return result +def select_join_serialize( # noqa: C901 + row: R | Sequence[R], + relationships: list[str] | None = None, + return_as_dict: bool = False, # noqa: FBT001, FBT002 +) -> dict[str, Any] | list[dict[str, Any]] | tuple[Any, ...] | list[tuple[Any, ...]] | None: + """ + 将 SQLAlchemy 连接查询结果序列化为支持属性访问的 namedtuple,支持虚拟关系嵌套 + + 扁平序列化:relationships=None + + 嵌套序列化: + - row = select(User, Dept).join(...).all() + - relationships = ['User-m2o-Dept'] + - 输出:Result(name='Alice', dept=Dept(...)) + + :param row: SQLAlchemy 查询结果 + :param relationships: 虚拟关系 (source_model_class-type-target_model_class, type: o2m/m2o/o2o/m2m) + :param return_as_dict: False 返回 namedtuple,True 返回 dict + :return: + """ + + def get_relation_key(target_: str, rel_type_: str) -> str: + return target_ if rel_type_ in ['o2o', 'm2o'] else target_ + 's' + + if not row: + return None + + is_single = not isinstance(row, list) + rows_list = [row] if is_single else row + + if not rows_list: + return None + + # 获取主结构,取第一行第一列,类似于 scalar() + first_row = rows_list[0] + main_obj = first_row[0] if hasattr(first_row, '__getitem__') and len(first_row) > 0 else first_row + + if main_obj is None: + return None + + main_cls = type(main_obj) + main_obj_name = main_cls.__name__.lower() + main_mapper = class_mapper(main_cls) + main_columns = [ + prop.key + for prop in main_mapper.iterate_properties + if isinstance(prop, (ColumnProperty, SynonymProperty)) and hasattr(main_obj, prop.key) + ] + + # 单一遍历收集所有必要信息 + sub_objs = {} + cls_idxs = {} + main_ids = set() + flat_grouped = defaultdict(lambda: defaultdict(list)) + all_layer_instances = defaultdict(lambda: defaultdict(lambda: defaultdict(list))) + sub_obj_names = set() + has_relationships = bool(relationships) + + if has_relationships: + relation_graph = defaultdict(dict) + reverse_relation = {} + for rel_str in relationships: + parts = rel_str.split('-') + if len(parts) != 3: + continue + source, rel_type, target = ( + parts[0].lower(), + parts[1].lower(), + parts[2].lower(), + ) + if rel_type not in ['o2m', 'm2o', 'o2o', 'm2m']: + continue + relation_graph[source][target] = rel_type + reverse_relation[target] = source + + for i, row in enumerate(rows_list): + if hasattr(row, '__getitem__'): + row_items = row + else: + row_items = (row,) + if row is None: + continue + + main = row_items[0] + if main is None: + continue + + main_id = getattr(main, 'id', None) or id(main) + main_ids.add(main_id) + flat_grouped[main_id]['main'] = main # 只在第一次设置 main + if i == 0: + cls_idxs[main_obj_name] = 0 + + for j, obj in enumerate(row_items[1:], 1): + if obj is None: + continue + cls_name = type(obj).__name__.lower() + if cls_name not in sub_obj_names: + sub_obj_names.add(cls_name) + sub_mapper = class_mapper(type(obj)) + sub_objs[cls_name] = [ + prop.key + for prop in sub_mapper.iterate_properties + if isinstance(prop, (ColumnProperty, SynonymProperty)) + ] + if i == 0: + cls_idxs[cls_name] = j + flat_grouped[main_id][cls_name].append(obj) + + if has_relationships and cls_name in reverse_relation: + parent_layer = reverse_relation[cls_name] + parent_idx = cls_idxs.get(parent_layer, 0) + parent_obj = row_items[parent_idx] + if parent_obj is None: + continue + parent_id = getattr(parent_obj, 'id', None) + if parent_id is None: + continue + all_layer_instances[main_id][cls_name][parent_id].append(obj) + + if not main_ids: + return None + + group_keys = dict.fromkeys(sub_obj_names, 'id') + + # 子类型预计算 + sub_types = {} + sub_full_fields = {} + for sub_obj_name, sub_columns in sub_objs.items(): + child_rel_keys = ( + sorted(starmap(get_relation_key, relation_graph[sub_obj_name].items())) if has_relationships else [] + ) + full_columns = sub_columns + child_rel_keys + sub_full_fields[sub_obj_name] = full_columns + if not return_as_dict and full_columns: + if child_rel_keys: + sub_types[sub_obj_name] = namedtuple(sub_obj_name.capitalize(), full_columns) # noqa: PYI024 + else: + sub_types[sub_obj_name] = namedtuple(sub_obj_name.capitalize(), sub_columns) # noqa: PYI024 + + if has_relationships: + # 顶级收集 + top_level_subs = sorted(relation_graph[main_obj_name].keys()) + for main_id in main_ids: + for layer in top_level_subs: + if layer in cls_idxs: + for obj in flat_grouped[main_id].get(layer, []): + all_layer_instances[main_id][layer][main_id].append(obj) + + # 预计算主结果字段 + result_fields = main_columns.copy() + if not return_as_dict: + top_keys = [ + get_relation_key(top_layer, relation_graph[main_obj_name][top_layer]) for top_layer in top_level_subs + ] + result_fields.extend(top_keys) + result_type = namedtuple('Result', result_fields) if result_fields else None # noqa: PYI024 + + # 递归构建 + @lru_cache(maxsize=128) + def build_nested(layer_name: str, parent_id_: int, main_id_: int) -> list: + objs_list = all_layer_instances[main_id_][layer_name].get(parent_id_, []) + layer_key = group_keys.get(layer_name, 'id') + + node_map_ = {} + for obj_ in objs_list: + layer_id = getattr(obj_, layer_key, None) + if layer_id is not None and layer_id not in node_map_: + node_map_[layer_id] = obj_ + + instances = [] + sub_columns_ = sub_objs.get(layer_name, []) + child_rels = sorted(relation_graph[layer_name].items(), key=operator.itemgetter(0)) + sub_full_fields.get(layer_name, []) + + nt_type = sub_types.get(layer_name) if not return_as_dict else None + + for obj_ in node_map_.values(): + nest_dict = {col: getattr(obj_, col, None) for col in sub_columns_} + + for child_name_, rel_type_ in child_rels: + child_parent_id = getattr(obj_, group_keys.get(layer_name, 'id'), None) + if child_parent_id is None: + continue + child_instances = build_nested(child_name_, child_parent_id, main_id_) + child_key = get_relation_key(child_name_, rel_type_) + if rel_type_ in ['m2o', 'o2o']: + nest_dict[child_key] = child_instances[0] if child_instances else None + else: + nest_dict[child_key] = child_instances + + if return_as_dict: + instances.append(nest_dict) + else: + instances.append(nt_type(**nest_dict)) + return instances + + # 构建结果 list + result_list = [] + sorted_main_ids = sorted(main_ids) # 排序以保持确定性 + for main_id in sorted_main_ids: + main = flat_grouped[main_id]['main'] + nest_data = {col: getattr(main, col, None) for col in main_columns} + if has_relationships: + for top_layer in top_level_subs: + rel_type = relation_graph[main_obj_name][top_layer] + top_key = get_relation_key(top_layer, rel_type) + top_instances = build_nested(top_layer, main_id, main_id) + if rel_type in ['m2o', 'o2o']: + nest_data[top_key] = top_instances[0] if top_instances else None + else: + nest_data[top_key] = top_instances + else: + for sub_obj in sorted(sub_obj_names): + subs = flat_grouped[main_id].get(sub_obj, []) + sub_columns = sub_objs.get(sub_obj, []) + group_key = group_keys.get(sub_obj, 'id') + node_map = {} + for obj in subs: + obj_id = getattr(obj, group_key, None) + if obj_id is not None and obj_id not in node_map: + node_map[obj_id] = obj + unique_objs = list(node_map.values()) + if not unique_objs: + nest_data[sub_obj] = [] + elif len(unique_objs) == 1: + if return_as_dict: + nest_data[sub_obj] = {col: getattr(unique_objs[0], col, None) for col in sub_columns} + else: + nt_type = sub_types.get(sub_obj) + nest_data[sub_obj] = nt_type(**{col: getattr(unique_objs[0], col, None) for col in sub_columns}) + else: + if return_as_dict: + nest_data[sub_obj] = [ + {col: getattr(obj, col, None) for col in sub_columns} for obj in unique_objs + ] + else: + nt_type = sub_types.get(sub_obj) + nest_data[sub_obj] = [ + nt_type(**{col: getattr(obj, col, None) for col in sub_columns}) for obj in unique_objs + ] + + if not return_as_dict: + if has_relationships: + result_list.append(result_type(**nest_data)) + else: + result_fields = main_columns + sorted(sub_obj_names) + result_type = namedtuple('Result', result_fields) # noqa: PYI024 + result_list.append(result_type(**nest_data)) + else: + result_list.append(nest_data) + + return result_list[0] if len(result_list) == 1 else result_list + + class MsgSpecJSONResponse(JSONResponse): """ 使用高性能的 msgspec 库将数据序列化为 JSON 的响应类 diff --git a/requirements.txt b/requirements.txt index 5f8dc94f..2d116a53 100644 --- a/requirements.txt +++ b/requirements.txt @@ -30,7 +30,7 @@ bidict==0.23.1 # via python-socketio billiard==4.2.2 # via celery -cappa==0.30.2 +cappa==0.30.4 # via fastapi-best-architecture celery==5.5.3 # via @@ -77,7 +77,7 @@ distlib==0.4.0 # via virtualenv dnspython==2.8.0 # via email-validator -dulwich==0.24.7 +dulwich==0.24.8 # via fastapi-best-architecture ecdsa==0.19.1 # via python-jose @@ -89,7 +89,7 @@ exceptiongroup==1.3.0 ; python_full_version < '3.11' # pytest fast-captcha==0.3.2 # via fastapi-best-architecture -fastapi==0.120.2 +fastapi==0.121.0 # via # fastapi-best-architecture # fastapi-limiter @@ -106,7 +106,7 @@ flower==2.0.1 # via fastapi-best-architecture gevent==25.9.1 # via fastapi-best-architecture -granian==2.5.5 +granian==2.5.7 # via fastapi-best-architecture greenlet==3.2.4 # via @@ -177,7 +177,7 @@ prometheus-client==0.23.1 # via flower prompt-toolkit==3.0.52 # via click-repl -psutil==7.1.2 +psutil==7.1.3 # via fastapi-best-architecture psycopg==3.2.10 # via fastapi-best-architecture @@ -191,14 +191,14 @@ pyasn1==0.6.1 # rsa pycparser==2.23 ; implementation_name != 'PyPy' and platform_python_implementation != 'PyPy' # via cffi -pydantic==2.12.3 +pydantic==2.12.4 # via # fastapi # fastapi-best-architecture # fastapi-pagination # pydantic-settings # sqlalchemy-crud-plus -pydantic-core==2.41.4 +pydantic-core==2.41.5 # via pydantic pydantic-settings==2.11.0 # via fastapi-best-architecture @@ -267,7 +267,7 @@ sqlalchemy-crud-plus==1.13.0 # via fastapi-best-architecture sqlparse==0.5.3 # via fastapi-best-architecture -starlette==0.49.1 +starlette==0.49.3 # via # fastapi # starlette-context diff --git a/uv.lock b/uv.lock index 674beac1..9441553b 100644 --- a/uv.lock +++ b/uv.lock @@ -284,16 +284,16 @@ wheels = [ [[package]] name = "cappa" -version = "0.30.2" +version = "0.30.4" source = { registry = "https://mirrors.aliyun.com/pypi/simple" } dependencies = [ { name = "rich" }, { name = "type-lens" }, { name = "typing-extensions" }, ] -sdist = { url = "https://mirrors.aliyun.com/pypi/packages/d9/27/c8f054ff37ea59fc656002dff09e025045844f089a5043cffbc70e118688/cappa-0.30.2.tar.gz", hash = "sha256:9d964dfea9a72a0e07b64d09617bd58a25b1b6dd97c31fe33d21edfccaf98e1f" } +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/3a/f0/6ed2611b352f1e7040e1d9f579acd758be88463a32b07293c5dcf749c656/cappa-0.30.4.tar.gz", hash = "sha256:06870e8cd67636039d0c134dae7a88fe2a383e13fd3310fe5c36fd5c7eed6d52" } wheels = [ - { url = "https://mirrors.aliyun.com/pypi/packages/d1/67/32afa9a82215c3ec883691bbfc811bddb574d3806211b20388d3ed61df7a/cappa-0.30.2-py3-none-any.whl", hash = "sha256:0b2da6d431447d988165539aed53bd410ccdc59d7bfa5bb99684c80ea9f0eb11" }, + { url = "https://mirrors.aliyun.com/pypi/packages/37/8a/4220ff1b14233d2fd7a09b601c5212356fd968d7db96cfb1bced33e18ada/cappa-0.30.4-py3-none-any.whl", hash = "sha256:9921f888e74f4af1421c5bd9368ff9c50dbf243c519246c6f79c2a536288f180" }, ] [[package]] @@ -570,39 +570,39 @@ wheels = [ [[package]] name = "dulwich" -version = "0.24.7" +version = "0.24.8" source = { registry = "https://mirrors.aliyun.com/pypi/simple" } dependencies = [ { name = "typing-extensions", marker = "python_full_version < '3.12'" }, { name = "urllib3" }, ] -sdist = { url = "https://mirrors.aliyun.com/pypi/packages/3a/40/ac6b7a749395edacd057278ac30cd66f15261581fe20c440327aa6fa9c5e/dulwich-0.24.7.tar.gz", hash = "sha256:f10bffa1395a8dedc3d38ac05164f761ae838a6a18f9a09a966f27dd651850d4" } -wheels = [ - { url = "https://mirrors.aliyun.com/pypi/packages/b2/25/f576e3415c4434b01dd42eef58f2093babe51a7e9854edd86c03ab7833ac/dulwich-0.24.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d55ccb9b89c00334a372bd1a8ac8203669251af0d1747dff36730eca617f941f" }, - { url = "https://mirrors.aliyun.com/pypi/packages/ac/9b/a54fdcad2d9a756a3e22a834bc8e34515a9b787224256cc655d5f8432915/dulwich-0.24.7-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:03baba91e4c143e8cacf1e55c91061a401a266de4ced5904909825a934b43cbb" }, - { url = "https://mirrors.aliyun.com/pypi/packages/d8/06/1d4bf1fca40fb3bb65eaccb6e8ec069ac8f58a11ba4bb179896cd434b1e9/dulwich-0.24.7-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:1476379539c60a689ccfefbde3ec3b5e584cd6fc8669de89e69c4ea6a404520f" }, - { url = "https://mirrors.aliyun.com/pypi/packages/c7/6c/8e680917c4cc26571ba42672a44f1d22cf570f5c6840abf861730cbded8c/dulwich-0.24.7-cp310-cp310-win32.whl", hash = "sha256:e3675065022ec06a9ddc339e317900a1b4e6cc020516704e7c58eb3ba990458a" }, - { url = "https://mirrors.aliyun.com/pypi/packages/9c/28/f32471fe5d72084183651e61b6c64767b2b2b4540312193e88077d6f44b8/dulwich-0.24.7-cp310-cp310-win_amd64.whl", hash = "sha256:7c9f59c4089667f64e9f6c3711a444067882f1ae3d27e6475acf2c12ec9aeadc" }, - { url = "https://mirrors.aliyun.com/pypi/packages/b2/25/9ecbd82016a27cc9599554039adc1ac3b5ab89020b65493d864c685e7183/dulwich-0.24.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:381baadd6b4af9264586b91fe09260ccf790b08dae5ef688645503c199094c7a" }, - { url = "https://mirrors.aliyun.com/pypi/packages/b7/6e/f6c2da48ffbfa94938579efec5fb9071dc345585327ce8d545eb12c97ea5/dulwich-0.24.7-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:f88c2e7f6c369f2d988e14b76d2228a48d3aea475e7ff13ceb156edb3a18b2f9" }, - { url = "https://mirrors.aliyun.com/pypi/packages/5e/e3/f94789b688b56e9b8383af7745605cd5b3c6ad1c835e7b0228a1a33e7313/dulwich-0.24.7-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:bb12ace558b3f4ba37bbd069d0f6bd4add7416693a3bc95e3286c5acff0c05a1" }, - { url = "https://mirrors.aliyun.com/pypi/packages/62/b7/c4eb362b5cd3138835519592190f1662055ca1b323c82bc9f2d03ee5c1d5/dulwich-0.24.7-cp311-cp311-win32.whl", hash = "sha256:ae60eec025625e65f7caf37a69076b12cdb4b63ddb317990ff0cb817c8132988" }, - { url = "https://mirrors.aliyun.com/pypi/packages/67/17/0bc77b51263b0bca688056afb608645cdd5458f84bb368138b0abf45846f/dulwich-0.24.7-cp311-cp311-win_amd64.whl", hash = "sha256:d328200d3caa20557e0568e8dd6c46714406b45abd4e0b333eeb5436614e4f3c" }, - { url = "https://mirrors.aliyun.com/pypi/packages/f1/0d/70e305ff1a666f157acac2bc3c73c94ce267e9112173fa2fcf852216430f/dulwich-0.24.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5cb46d76558147529d8de295f8ca22bce0a4cb62163f708c54fc92f2a8d8728f" }, - { url = "https://mirrors.aliyun.com/pypi/packages/6f/dc/e3628cc61ecc3ff7193639728f2c2cea8865d4e0e355edd8f941a441639c/dulwich-0.24.7-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:ba0a121b148dffa5cc3d5fdceb32a002cb6b75a6b79abd89750584baa5021c0b" }, - { url = "https://mirrors.aliyun.com/pypi/packages/11/09/2a70e2bbf07ee7be5b7d5c9c4324fb18a54df99d174daca6e84c3c3f1bb5/dulwich-0.24.7-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:23182ca6bd54b74109c2fb2bb70b6c34e7dc3bbcc08ecb5c6c31a3a4aa1b30c3" }, - { url = "https://mirrors.aliyun.com/pypi/packages/5c/37/f229c33be8104703a62364f33d10582a278356f4b6e2c1ab78d85cc73b89/dulwich-0.24.7-cp312-cp312-win32.whl", hash = "sha256:1c154a8f33acd815ad990e85d230497de892dde2476e35370f5762d34a1f70fa" }, - { url = "https://mirrors.aliyun.com/pypi/packages/2a/16/5afbd1ef7927f9d0bc230121c94a991b41ed667a9e033603c9919118e7d3/dulwich-0.24.7-cp312-cp312-win_amd64.whl", hash = "sha256:19f7a90377f5814716beaaeec34897d181c200a666baf477650e0cd4c699443f" }, - { url = "https://mirrors.aliyun.com/pypi/packages/bf/09/f24980d91916c5c374b0749071d1d531a072242905c5746787531e3db32c/dulwich-0.24.7-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:461f2a894e1045fc6faad1ca0123aac87554f5dd40cbb5772e35933f1f6f1e32" }, - { url = "https://mirrors.aliyun.com/pypi/packages/58/72/7c122b7e3ea8d98219df58e63abbd55b5b1980bd491ac81a32dfbebf9eec/dulwich-0.24.7-cp313-cp313-android_21_x86_64.whl", hash = "sha256:c8f44cb89d85fe40fa91ec34190d70a016be917ee841591fdbe9fd7e0ff61fc2" }, - { url = "https://mirrors.aliyun.com/pypi/packages/4a/e8/76c642258967a694fb313a3fbc0f0ca3c376292f0de60654f91cd0eefebe/dulwich-0.24.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:06219dd38d553f18184dc885fbabe27d3d344ab0327d4ab3f1606617c09d8b32" }, - { url = "https://mirrors.aliyun.com/pypi/packages/78/ec/c422e81037a537dac21a18357e1e828e67346c6f3af101821c3a089745b2/dulwich-0.24.7-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:3eb5af24dd2c760701f4b7780b1a9fb5a0da731053fe5d1d0a2f92daa4c62240" }, - { url = "https://mirrors.aliyun.com/pypi/packages/91/c3/bfaf8426ebd44d4834f7578e543727543a2ccdda8e7b40be919b857872b5/dulwich-0.24.7-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:d8c42e45c217506759170b01b09e194acce1463aafd61f71fb7094b192ad09aa" }, - { url = "https://mirrors.aliyun.com/pypi/packages/8a/52/3a7d40831ba5ab0701fc3bf67d28cc10c4fcddfc8ae5a600232837e1ffe1/dulwich-0.24.7-cp313-cp313-win32.whl", hash = "sha256:32e7e18edfad5dfb978ccf8e575b5776234110b722ea677d4e843058a1df1dd0" }, - { url = "https://mirrors.aliyun.com/pypi/packages/5a/b7/8d026a8ee3186c3a939ae41248eee47b374427547bd660088f4b8beb5920/dulwich-0.24.7-cp313-cp313-win_amd64.whl", hash = "sha256:265549c96680be1f6322cfeabb41337714c1a128824ab7e06d8c9d8a2640f4fb" }, - { url = "https://mirrors.aliyun.com/pypi/packages/0a/86/6c55c8ed6458be5eb6bbbf70190ed9dc1d3d6d7999cae1b92f6f595ad5c2/dulwich-0.24.7-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:d29784211e7aeb84ddca1265fe7b8356556f8da531432b542084fb70e8341a00" }, - { url = "https://mirrors.aliyun.com/pypi/packages/42/3d/68a11ed26d10aeb421f500a3b6e42c959763ef93bb2c5a00d6a75ef73750/dulwich-0.24.7-cp314-cp314-android_24_x86_64.whl", hash = "sha256:4bb0673480005c7aa331b05af77795f2612e5155fbecaaa0c3fd9da665dad420" }, - { url = "https://mirrors.aliyun.com/pypi/packages/14/30/d5c337fcf96e2b159b61053fd5b374b80629d28267f471483a3da5577ce3/dulwich-0.24.7-py3-none-any.whl", hash = "sha256:c1d6e35d7c41982d4ba375ce8ba9db783f4b4ca1a00c62f3121eb881f5c03c53" }, +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/58/98/b6b8bf80e61d1aacf59aad4e45e6a1e7c39751c7bcaeb1136821bae82cd8/dulwich-0.24.8.tar.gz", hash = "sha256:c9f4748bbcca56fb57458c71c0d30e2351ac15e0583d428c739c09228be68f05" } +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/63/1d/a267497da62d46afaa249be0f5cb6101333262175ff8a19e4379251003fd/dulwich-0.24.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:661af1fa3852d970fef68b0ab431f0bd488c3f94306e89244c173c4e6abb978e" }, + { url = "https://mirrors.aliyun.com/pypi/packages/5b/be/ff35cf15cca22a02037e783f1363b9df96a220058eb65bd83c769f15a46d/dulwich-0.24.8-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:1bac020051cf228b33c787294e17ac80a284e028c3749437ee72577ee04e1cd9" }, + { url = "https://mirrors.aliyun.com/pypi/packages/88/6b/524f93ac2f969e917f1afdd1aef7510d925ad1688a202acc5f2bcaecd019/dulwich-0.24.8-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:97f7c64b02fbd366c36557aa2e8642fc686aaec7d6569bc1460d0b38a63169f9" }, + { url = "https://mirrors.aliyun.com/pypi/packages/96/2d/0909df21a1adede54957ce9cbc4ca0bafb6270c49d2726b32ea4e2f96ff7/dulwich-0.24.8-cp310-cp310-win32.whl", hash = "sha256:f7519f3b8c66ba2e4ea66f47c2156a66cefedce2a121ac3227e030abe95698f3" }, + { url = "https://mirrors.aliyun.com/pypi/packages/c9/d3/1a2475f068c4de27919c1ac85ebc6cb7bf8a06af4be5afe89bc6e999c8aa/dulwich-0.24.8-cp310-cp310-win_amd64.whl", hash = "sha256:a60f8a5d718c7cc1f60bb931cc915311fd5198d85d68dde0ef3c569311c14a70" }, + { url = "https://mirrors.aliyun.com/pypi/packages/94/2d/71e1ef409c1dc0bdbafada47f4c76ac143265d674e8965715dbe074bdc84/dulwich-0.24.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:07aa6e7d41358fcba2a8ac53731e1b8ab201cac7a192ec678ef0da34c7643cf1" }, + { url = "https://mirrors.aliyun.com/pypi/packages/19/5a/9e9a5593cd048ebc909e1488c436381e4089555d2417685392054dc58ac4/dulwich-0.24.8-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:0e9aacbbb0b0cf4b3fecac2c29ddd4d4e36f03ced30851889c193986e8bb327e" }, + { url = "https://mirrors.aliyun.com/pypi/packages/52/f0/26766a38b50502ec132343fffbd66b15f97218d338b212a6166d78b670b4/dulwich-0.24.8-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:8eca5242f8aed324394c95ecd13a0a66d2a0c31c2556f0a52e7bb8dd67edef20" }, + { url = "https://mirrors.aliyun.com/pypi/packages/3f/e1/fbb694f113c491a5b705040705affdec032a95857eda5cb363b4d6de23da/dulwich-0.24.8-cp311-cp311-win32.whl", hash = "sha256:108c74b329599931bfe66c4a34fb9312cd8136053cbfc04e7007e7c34081c6b7" }, + { url = "https://mirrors.aliyun.com/pypi/packages/2f/f8/ab403d8b5011c6b4481999da56766a5f1eb572e5af1db0d4d41aab5e3f9b/dulwich-0.24.8-cp311-cp311-win_amd64.whl", hash = "sha256:efbf0f29d8d3d56a098e2b4a9260bdfa5f313142180a882c7b28e648d9b5ca9e" }, + { url = "https://mirrors.aliyun.com/pypi/packages/d5/56/344962feb31a5923235f06f85c55754664603a07976f35b32d0a0beae1a4/dulwich-0.24.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8ac85e3ea42878fa91b6a9282652327df54f5abea4aaf674036e1000608a15f0" }, + { url = "https://mirrors.aliyun.com/pypi/packages/c6/ba/3b105687dd924a7e07e97a75cdcdbc548c34862bb6b5da6c53910bd4b88c/dulwich-0.24.8-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:b31e08bcd0a4eb29915987fa5273a827cccca7ee83eb27ef17bd5899f5668055" }, + { url = "https://mirrors.aliyun.com/pypi/packages/ba/0c/732b159f9724ddc6b15e93056fbfac27b9f5f7fff8b2f6524c0815c901da/dulwich-0.24.8-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:267e79367cd6f438091248c1e826c6cf7abd84d5b641b9fe46fbc4d9119e11ed" }, + { url = "https://mirrors.aliyun.com/pypi/packages/49/ab/42b037616ebbc3b31723518f53eb1cb667b3504745f9a8089a062ae3fef9/dulwich-0.24.8-cp312-cp312-win32.whl", hash = "sha256:6a51a41e858e0427b14bb19df7ac1207275dd6b5cc1976f54710bf15cb4c5614" }, + { url = "https://mirrors.aliyun.com/pypi/packages/50/f9/cfc561df968ba3dcadf1666437502c6245ee823c8f12a0bae1f5d2b4df03/dulwich-0.24.8-cp312-cp312-win_amd64.whl", hash = "sha256:6016e3f7a0f1dd5e19df14b772cb8f42bfde0cd55c504642c05e1e8328de21e3" }, + { url = "https://mirrors.aliyun.com/pypi/packages/4a/95/cb0e005f055bdf84d5e9dd45e657ecea94fd8279f61a4921eafbeec569af/dulwich-0.24.8-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:19be46710a9d810a66d4a925754abf3818ba22d61134e7d7e1d7b1585c9445b6" }, + { url = "https://mirrors.aliyun.com/pypi/packages/d5/3c/33de9890a9c02b3f026fccb83a5672434cc5d667af53ac042812d29182e0/dulwich-0.24.8-cp313-cp313-android_21_x86_64.whl", hash = "sha256:17d8223cc69cf79ddd7a2f0893e223708f7efc2ea372f919ddcc0d852b3f5d06" }, + { url = "https://mirrors.aliyun.com/pypi/packages/57/e3/5f3b699486fddf021bb2af2edc7cdde37e5124682813b82c53be12864648/dulwich-0.24.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a0a780b512d4144336eac2f1f6b982eb78c313442e88ba0db853a224e2b918ef" }, + { url = "https://mirrors.aliyun.com/pypi/packages/d3/9a/45fdf7e12a5b1cc9971a14644c4125eda8279f70f89dd1b38259700cf3e3/dulwich-0.24.8-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:2e3a9a713fda94f3216da4743db3cc8d670330f44c4d98580ac4600242dba2c4" }, + { url = "https://mirrors.aliyun.com/pypi/packages/b2/6d/459aeaa32c503a51649179e43ea4ffb35bee9b4ca62955e8dcce6abefab1/dulwich-0.24.8-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:b03474c16bcfa3524241b4ae89007931d79ac37c0b19e059133a6e63b7039845" }, + { url = "https://mirrors.aliyun.com/pypi/packages/44/15/a02a2a7d29c28440a54f9b8494d87022bf215ca63ac9032e8c15f9f743a8/dulwich-0.24.8-cp313-cp313-win32.whl", hash = "sha256:ec0f62538b6fb26cdd1b2fb70788ccfdb17df26a4ba1ca70e623e196c4004f5c" }, + { url = "https://mirrors.aliyun.com/pypi/packages/af/15/0a589d57e79b15a33e09849f2273d73edce623b4e6ac2e8f37573cacad51/dulwich-0.24.8-cp313-cp313-win_amd64.whl", hash = "sha256:16e335bce0d5192d476db0ca81de1f90fb56863ad7d0b985b0333a8194c73c64" }, + { url = "https://mirrors.aliyun.com/pypi/packages/68/ea/9227599be958b387d678cdd1d371b97dbc928597848dc25be41a664932c0/dulwich-0.24.8-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:19855e8a0ce299cdcdafdc8bc4f6653bea9e02124a5022e13cda8103fb36912d" }, + { url = "https://mirrors.aliyun.com/pypi/packages/48/f8/c308bf0e4ad47f66292bbca5aaa2b019ee73d71f6ce3ef41ef0e5d7d6d58/dulwich-0.24.8-cp314-cp314-android_24_x86_64.whl", hash = "sha256:da03c7a6629b7ed37e7139739a175f2c9678080a45444418c54ab28d2ec6524b" }, + { url = "https://mirrors.aliyun.com/pypi/packages/28/d7/ace65f3aa8af1994443dfa741311e8995e0e269397d8336533f064458404/dulwich-0.24.8-py3-none-any.whl", hash = "sha256:6ffdd616135bcb31eb2edcccf82d4408720f1db69f596f687ffa2d26c2f5e6f4" }, ] [[package]] @@ -656,7 +656,7 @@ wheels = [ [[package]] name = "fastapi" -version = "0.120.2" +version = "0.121.0" source = { registry = "https://mirrors.aliyun.com/pypi/simple" } dependencies = [ { name = "annotated-doc" }, @@ -664,9 +664,9 @@ dependencies = [ { name = "starlette" }, { name = "typing-extensions" }, ] -sdist = { url = "https://mirrors.aliyun.com/pypi/packages/a0/fb/79e556bc8f9d360e5cc2fa7364a7ad6bda6f1736938b43a2791fa8baee7b/fastapi-0.120.2.tar.gz", hash = "sha256:4c5ab43e2a90335bbd8326d1b659eac0f3dbcc015e2af573c4f5de406232c4ac" } +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/8c/e3/77a2df0946703973b9905fd0cde6172c15e0781984320123b4f5079e7113/fastapi-0.121.0.tar.gz", hash = "sha256:06663356a0b1ee93e875bbf05a31fb22314f5bed455afaaad2b2dad7f26e98fa" } wheels = [ - { url = "https://mirrors.aliyun.com/pypi/packages/81/cc/1c33d05f62c9349bb80dfe789cc9a7409bdfb337a63fa347fd651d25294a/fastapi-0.120.2-py3-none-any.whl", hash = "sha256:bedcf2c14240e43d56cb9a339b32bcf15104fe6b5897c0222603cb7ec416c8eb" }, + { url = "https://mirrors.aliyun.com/pypi/packages/dd/2c/42277afc1ba1a18f8358561eee40785d27becab8f80a1f945c0a3051c6eb/fastapi-0.121.0-py3-none-any.whl", hash = "sha256:8bdf1b15a55f4e4b0d6201033da9109ea15632cb76cf156e7b8b4019f2172106" }, ] [package.optional-dependencies] @@ -912,95 +912,95 @@ wheels = [ [[package]] name = "granian" -version = "2.5.5" +version = "2.5.7" source = { registry = "https://mirrors.aliyun.com/pypi/simple" } dependencies = [ { name = "click" }, ] -sdist = { url = "https://mirrors.aliyun.com/pypi/packages/07/85/3f5a1258567718c75719f5206d33457f7bd2b091b0fee0a618a395fda758/granian-2.5.5.tar.gz", hash = "sha256:da785fae71cb45e92ce3fbb8633dc48b12f6a5055a7358226d78176967a5d2c9" } -wheels = [ - { url = "https://mirrors.aliyun.com/pypi/packages/c1/d8/b0c4318f6fd8583d9ee2902d555985643c3b825819a85465ff01030ea29b/granian-2.5.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:64a249ba2e04499f63636c5aca5ef0a1eef7768a8fa0ebc3d9c05611397dd907" }, - { url = "https://mirrors.aliyun.com/pypi/packages/8d/57/98c60a37575570acaabc31614304e87ecaf5a3b7da636c84248eebe8020b/granian-2.5.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:478e3c7416060fa7ead59e49887f18932c93f3c5f933c69935d89db653054bd7" }, - { url = "https://mirrors.aliyun.com/pypi/packages/50/71/96776e0a462483a8e78c3ac8d8bb30f0dc3c5a01a7f3f0c170cdbd62011f/granian-2.5.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7369dc8f69cbc4b4f9eba7e63879aa9f2842c9d42eb32373317d9325e5556d1c" }, - { url = "https://mirrors.aliyun.com/pypi/packages/c9/7a/a72f9724b8d6b665089aa899bf709c1a392de9edea686a0bcfb42f96d402/granian-2.5.5-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:861c4c698eba03eb6967444f1f0ef82708deda10daff9d0968762680c491cc5f" }, - { url = "https://mirrors.aliyun.com/pypi/packages/2d/d3/6e5d600bffe6cc52c217625db198c8f98cc92a7a82e6a6be3fad88580fd0/granian-2.5.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b6ef2c32cf0f99bfc6ccd75bdbce8914f3e64040ad297a6a53b354ffd97e5eed" }, - { url = "https://mirrors.aliyun.com/pypi/packages/ae/5b/f06f2c80eece08c04d588a6f1bfd007cc6b18c1cf30e40bf445b47ba4777/granian-2.5.5-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:d1bb0f3f105db479c274a94c7cb81b7328a25282906cec2c2171974da3b3006f" }, - { url = "https://mirrors.aliyun.com/pypi/packages/73/3c/65614725553d8bde43cb7e2a1cba0d3639ccfcefd4f44a1a465cc1ad2c51/granian-2.5.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:72661830c9ced3b34e32d10731ebca2cd1b882343ebdd77f39e3cd4140cf181f" }, - { url = "https://mirrors.aliyun.com/pypi/packages/4e/d9/8c845dd41c0be816d0de7cece506e5e12c9461810e01cfc47a5aeb979518/granian-2.5.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:dc2906c3fac1b039bfd045b829a2cf0a70f2a71a34835e4f1e41a0a374564722" }, - { url = "https://mirrors.aliyun.com/pypi/packages/0b/0e/a022ac71e5a20e61b892e54ab883e727ccae2e7ca0bc4df5fb15c55ae961/granian-2.5.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d6ab38e7774e27cc3a2f50d7673c32d736dfcf9ac7d0d00e9a30598611f7b2c6" }, - { url = "https://mirrors.aliyun.com/pypi/packages/b1/3f/6d607ca0dc6726f5f2a72a0e77003b0bc35c1c94b1be8d89a2bbbdf5e352/granian-2.5.5-cp310-cp310-win_amd64.whl", hash = "sha256:e1a4f75b0dedc12832170c46bdaf522fecbfa2a32cd95359531daa1f895e0c27" }, - { url = "https://mirrors.aliyun.com/pypi/packages/6b/e9/98a051959a31f1aacc7b9306970b95c198d2d99c3fb4abbf008b55a6f2d9/granian-2.5.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:36af3b59045bcac79d3fe5d347e0d207ee15efb8967c8af743b967104507dfb1" }, - { url = "https://mirrors.aliyun.com/pypi/packages/ec/4c/9af839f55e02e7302d2354f40161c95965216bd469d4ac09cac0fca3b9d7/granian-2.5.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f0d3c1fd63c792b903e36cca03960716a66c3e9ce0e439393051883d6d0a9fbb" }, - { url = "https://mirrors.aliyun.com/pypi/packages/40/32/092dc35a974cec96600f87ec22bf3fe794ff143b8d5ea05278e9e8e08027/granian-2.5.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a325fa91d44dee738f7c09f1602aca0c5ea0986c87ae6bb30eb7e41c7ca32bfc" }, - { url = "https://mirrors.aliyun.com/pypi/packages/4b/7f/2533a2b9f1894fd6f2e9102818777bc9ff012f59b6dc3306a054c7b1b3c7/granian-2.5.5-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:940c8f72601ebf0782aff6abe8f93317912f485806742dee815940a7eeb408ba" }, - { url = "https://mirrors.aliyun.com/pypi/packages/5e/5c/f8d95c024cee7fe6e8a63d3c2fd193726f6627b9da833781246fa639550b/granian-2.5.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cbb009686ae57f2e6e0d2bc16b14a3124aaf6c64e1d9eff12563a939b9da6ce3" }, - { url = "https://mirrors.aliyun.com/pypi/packages/db/77/a2fb38eb9f04a8b8d9049c11e1d1ecdbfa7258015780444d94f1c3740575/granian-2.5.5-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:e5f027bf3814393c64eb9e2cdfc07fc425f97c5728bca32c7ec2cb65ca6f8521" }, - { url = "https://mirrors.aliyun.com/pypi/packages/95/20/7876edd6bc322f228aa25099f90a5f50ed54ed48d4c10d8d6843bdc49dd4/granian-2.5.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:126dd070d18eabe26be907bc1f48fd55983fa7e9b39e9e4050165bbbeae7459d" }, - { url = "https://mirrors.aliyun.com/pypi/packages/5c/e8/e05d8ef905660f4ccf736c8e3ecbc550d69c38c6f8573e9090e471dbbed8/granian-2.5.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:1c5de153519817a377f430f2501222d3092501daec34ae9fa7f5cc806ce98890" }, - { url = "https://mirrors.aliyun.com/pypi/packages/e6/cd/23b039aec4c0f3307f85a78faca8efe0b41c3cc0201f7dad410671580269/granian-2.5.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:b808a23cd48da428e4a49ba4643c872813ee41c80caffce87055abdfa8dba23b" }, - { url = "https://mirrors.aliyun.com/pypi/packages/56/ec/bf55d9e431bab8077e592be0597734e2cfddd9c4c65a7cc7a5243f9b766b/granian-2.5.5-cp311-cp311-win_amd64.whl", hash = "sha256:2354edca6787025a7e356b2afc05153943615c116084e7ea745fe528204dfb86" }, - { url = "https://mirrors.aliyun.com/pypi/packages/9b/92/e4ea9ba8d04d6e2196db3524caa9b58c4739e36c4e9dab69b0db7e5cbc2a/granian-2.5.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:4b146e435799aa09cd9ccc46498d217757f76b77c59961d17e0d933e7b54469a" }, - { url = "https://mirrors.aliyun.com/pypi/packages/d5/80/4d21a80f988a72173389663e2974417cc89bb00bdec11c23e53846478604/granian-2.5.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:909168f74eccee90e7909b0396ae407f6ec8cc7e793b8fe5ce84f522a3ef6b77" }, - { url = "https://mirrors.aliyun.com/pypi/packages/e4/69/16218292c97dbee42b1a38cb0db37866a90f5cafffd1ddf84648b39bb9f1/granian-2.5.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c4a853e3d6fc1ea8eb80c00bd794a887c885163e08d01848dd07aa6ffe68926f" }, - { url = "https://mirrors.aliyun.com/pypi/packages/09/48/ec988c6a5d025e1433d50f828893050d5228f4153a2c46b8d5967741c17f/granian-2.5.5-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6baa556ea84a078f5bb444615792032cfcfd2b6764e07915ecec0aec53f272f3" }, - { url = "https://mirrors.aliyun.com/pypi/packages/c8/92/2acfc39b45089702098c647e3417b9c05af4698e2f0d9b53292e56bf8eb9/granian-2.5.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce38e5cbb50d3098c8a79362e2b9e598d56001595860060185aa46f94a73776d" }, - { url = "https://mirrors.aliyun.com/pypi/packages/7b/43/80ff0139cc0973787433f6bfbe0b6ecb5a700ea39d8c85c1b9eca13b7e2b/granian-2.5.5-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:a40757947e422bed1c14703bbcb424417f1c2f9a27c74d19456b7b7af265992b" }, - { url = "https://mirrors.aliyun.com/pypi/packages/d7/49/a2fda46a999d97330a22de1e1b2213635b5e4a97e1ebd646ca2c74a6ef50/granian-2.5.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:806f8bb7b1483552a1a21820b242b395675d5011764dd0fabebc695f5d9f4bee" }, - { url = "https://mirrors.aliyun.com/pypi/packages/6c/13/7318be6322e0c4c5d196db44ff425df1e0574f224934507aa1093b872424/granian-2.5.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:bd670dc87c2d09d7373473b9d3330897207900e86c17a8220c4abec78ef4e4a7" }, - { url = "https://mirrors.aliyun.com/pypi/packages/35/34/eec8a8b57de273c0eb1593b607d443d311b6df2eb46db8c493b4ae039718/granian-2.5.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bdf7f68283c4449253f9bc57ac69d63216eacd463a97501915b5636386d12175" }, - { url = "https://mirrors.aliyun.com/pypi/packages/f3/2b/8455add059d45514d952bf9cf110ce3b3a9c0ecfaa63e2de07d994b40ed1/granian-2.5.5-cp312-cp312-win_amd64.whl", hash = "sha256:32e4a39f8850298f1fe6900a871db2a1440aba0208b39363e7ca96e81ef2340f" }, - { url = "https://mirrors.aliyun.com/pypi/packages/22/fc/8024558e038477531cdbf996f94ff9d64c008a33ffd33077b38d43451193/granian-2.5.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:49fd6a3f5d9302b8c8da56fcbf29faa7edc5847a460635356c5052584fa7c4b2" }, - { url = "https://mirrors.aliyun.com/pypi/packages/d9/6e/baae1148dc834bbdf07ca45920641c23ff167b2af338cfcd3551e063aee1/granian-2.5.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:62e7d9dd5b21d7637e062f7ce20d1887069d64d06e16c7bac40e01de4cb54b63" }, - { url = "https://mirrors.aliyun.com/pypi/packages/77/23/65398e16a121cdab95ac6a31c48172f86ff333ac01dbc8d57c2e9496c668/granian-2.5.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:307e09e1bf5b7f869b9dccfca7af253bbb74bf4cb0ba2d972043334a889b048f" }, - { url = "https://mirrors.aliyun.com/pypi/packages/24/01/1ec3bb03528cf5330a5b1d2c77f1a4d224a34a3c73ea47a860ef4d72146b/granian-2.5.5-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6a847903664275e6bede3ef178642379c6e1c43767e3980b2b6d68c62e6b14b5" }, - { url = "https://mirrors.aliyun.com/pypi/packages/1e/a5/fe976a36945cea22b7c4a9eb8ddd9866644c12f766b9f3ab8bd575e9d611/granian-2.5.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e21cd8ee95cd2017328043ec31c455ef69c9466617a9d51e4b1ca0ff0203d873" }, - { url = "https://mirrors.aliyun.com/pypi/packages/a2/f2/0a7450990a5ec23a3dc5c464886ece21593dc4edd027e3a6f5367fbb0cd4/granian-2.5.5-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:f47b9ecf635ef727b1f399cd2e65329010ab2c48a5381be0d928a538ed8e9158" }, - { url = "https://mirrors.aliyun.com/pypi/packages/93/81/1238e871ef9e609e12b9ada411793c87343c46905f8f56c8a6d4088d9ae6/granian-2.5.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:59e868dcc7ca469aa099ca9456204c2ff5e6c7e98bbafb66921b911c2b5e4c15" }, - { url = "https://mirrors.aliyun.com/pypi/packages/b0/3c/0ad5915c0a96d2c4de13e51f107d11013ed9a48e01524ec8cc096afdc639/granian-2.5.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:c0a1ef4bac9a59d52a80f81153a15c82a6876591e9ab8e330e096f339d06211d" }, - { url = "https://mirrors.aliyun.com/pypi/packages/75/f7/2d3d3372cf81775e72196a1a7b26cf563dbe35ec5cc197dd4f9e3df5d050/granian-2.5.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:748efbb431d46aff3b9ef88b68fddfc7b29e7738292fca841b1b925f67e328a4" }, - { url = "https://mirrors.aliyun.com/pypi/packages/8d/1b/d663e071cac94f3be515c3650aa6675432d3a9ccbb4c419a20bb31083d92/granian-2.5.5-cp313-cp313-win_amd64.whl", hash = "sha256:fa87dd2d4b914e6d539bf18968daad3d34bb6877ab90b1408c009c3714a0213c" }, - { url = "https://mirrors.aliyun.com/pypi/packages/9d/9f/edec61a604d919042b435eb6f40217c7ff565fde91e9670183c895eaf4e1/granian-2.5.5-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:647cb53789cbf369e895341152a9822f8238830fc939b85d2844151d1f0da32e" }, - { url = "https://mirrors.aliyun.com/pypi/packages/52/76/f96df41fbc72b4c9a9f0994f2ae274080a3b47b8209989140a1f98ba27a9/granian-2.5.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:39f1d5398d31df3375e10524aa00f33d65b7d051551c4876251e9eec4ddc2c11" }, - { url = "https://mirrors.aliyun.com/pypi/packages/e9/70/0bfa32321501756357efebc45668c9ba054f8b8af1c63590d5af2aaed59c/granian-2.5.5-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61308acf1943d06c4c4b48f79697a97f233ce85a07801e95e01c85647fd10eb5" }, - { url = "https://mirrors.aliyun.com/pypi/packages/3a/a1/9a961b05c6cde2b6b27abae5da9e5efdfe3c7e0fc8b6f63433473861c2ff/granian-2.5.5-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:dfd11cb106d9c68c6682a0554142f7383ff99789e1ecef3087c3e13ac50fde24" }, - { url = "https://mirrors.aliyun.com/pypi/packages/e2/1a/9eada34cb30c4cd17e8201082b2a5f4d159ed275fdbc072355bc8ab32753/granian-2.5.5-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:d05801e405f4000837bba9eea7cdef0486a6c8a2aff073e75be0a7055c2c07c6" }, - { url = "https://mirrors.aliyun.com/pypi/packages/5d/24/bbdc6dcb4197da663b2e8f4442f234ccfcd6fd883a9448a0fa3629cd6fe1/granian-2.5.5-cp313-cp313t-musllinux_1_1_armv7l.whl", hash = "sha256:28c61a87c766fc22f5d1ad640e6a4f2a4c14ff86c629c7fa8c9cc0bbc756a18c" }, - { url = "https://mirrors.aliyun.com/pypi/packages/0f/b9/a44efa4771859bd65ff1e103b57f1c010b8d3434b20120fc0e44d0f28b41/granian-2.5.5-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:1cf9431bc5096e2f38aa8b8c762ad4e5e2f1e119ca7381a3414e1ce64690ab5c" }, - { url = "https://mirrors.aliyun.com/pypi/packages/f9/03/3d33933328a2003ebd45c5d781387df59b60bc2465c9c5ea308930509ca7/granian-2.5.5-cp313-cp313t-win_amd64.whl", hash = "sha256:63a4910032589d25ce09bc2d1e5164db941710c17fe91a543f8bd3e3b9a5d522" }, - { url = "https://mirrors.aliyun.com/pypi/packages/d1/db/f9b2971eb3d409e46a2c432a7d0b621257eecc3ea383f5aad0ace6808453/granian-2.5.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:4c2c917decc7079038bc97a247ed8da35251a36c33211ae35698784a3d3dac7e" }, - { url = "https://mirrors.aliyun.com/pypi/packages/5a/9d/3922ff3298b801de6cf82405de88d5c2506f4c9f98dd0f42f78fd31018cf/granian-2.5.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fffb60591f1e5fc514b916689c69c3d4da7827c9989679b32c38416f5f968b5a" }, - { url = "https://mirrors.aliyun.com/pypi/packages/57/33/7378af82179009995183905403e5620ad149aaf58fb578f164853c93c9b9/granian-2.5.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b6d676c2b6a64aa5d377f8855c9921b637db150a7233c6027d3abd55d2d6c43c" }, - { url = "https://mirrors.aliyun.com/pypi/packages/5d/55/c5c9fb391dce4a606f7a8a264a1daf0f5c688a54b05366d4425d7eb12229/granian-2.5.5-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:642317efac6b24547c50a229bdeda85e79d825727a8559bb61e47827557d412f" }, - { url = "https://mirrors.aliyun.com/pypi/packages/a0/7e/2d541daff406d431830d6344a7c73547116a69d5bfe5116dbd586fd868f0/granian-2.5.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c40867944b278390b42f62f2557cc613dabc0a2a1868669110c319dfcd5dbe63" }, - { url = "https://mirrors.aliyun.com/pypi/packages/85/d9/daa2809ccbc11c0489fbb2ead8a084b902ba53bf1bf04a8268cbea911a2f/granian-2.5.5-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:bff88a7c1ad215a9bd33b2b11aa03057446f086759743be9f99ab1966604c733" }, - { url = "https://mirrors.aliyun.com/pypi/packages/70/c6/866916b6115a5a723733281b8731d1c7f0efc340f2541e2c9941c08541ca/granian-2.5.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:1701211bf09a3863c12346b1711d74cc47317809bb1b063b4198470877c351f6" }, - { url = "https://mirrors.aliyun.com/pypi/packages/f8/af/3d7ed403854324b5099b860c2e43d668dc98ec2e79c2cc9d246ccb8d2e98/granian-2.5.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:a3345512f0ce7dc481324a61932e397040d4c0ffbccbcbcbc3e41f21f397bb00" }, - { url = "https://mirrors.aliyun.com/pypi/packages/24/05/28352e5aa449e597c3e3d3d92b01c040eadcc389619023fa71c5ecba11de/granian-2.5.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:d3e56fdb95a692be0c9543f4bf902c8a00c9cb9cdcc7bcd4f1622d0eefd8df18" }, - { url = "https://mirrors.aliyun.com/pypi/packages/34/b7/4432e1c3efb43a7ee0ef068103acd2e53c42ad6cc226ff4da5e077b2f67d/granian-2.5.5-cp314-cp314-win_amd64.whl", hash = "sha256:4c772351cbbcc04a5b168e5cb574bff5fd6883d739ad395db1f0c330cccf8878" }, - { url = "https://mirrors.aliyun.com/pypi/packages/2d/f1/464811b150cea5aaf7f6f6a406b666b7c4daa36eef628a3e9a86c757a0ac/granian-2.5.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:94ec249a8b467d68cb0f24e482babb07110ff879a1cbcb841e7c55e1609e3777" }, - { url = "https://mirrors.aliyun.com/pypi/packages/0a/da/802a4ab9986cc7893b32977f310c285433730733d2629a8ad09caad3928e/granian-2.5.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d34b9626fef7f37b096b40316fa5167c40ba9326648112304aa7635397754f9e" }, - { url = "https://mirrors.aliyun.com/pypi/packages/de/59/32bad6d962ae2b63b30616183d9750b0ff84559099173c1aee6905fff8d2/granian-2.5.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:edaaca6d053e4e6795cee7cc939c9d97e2457bb5dadd96a91aeab16b4096816e" }, - { url = "https://mirrors.aliyun.com/pypi/packages/51/72/f6def6f64cc6194c23975ae853d32ebac0aff9cf69a749f3349ce5b1dcaf/granian-2.5.5-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:49c950366261470662b9721b40f08b8a443dceaa2ae952361235404f257f66cb" }, - { url = "https://mirrors.aliyun.com/pypi/packages/cb/65/059a411d9aef86b1fdd29d3bd274ac13e0568b18ea8f22b59322ba087fc4/granian-2.5.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:02d5c0f8d4fbb656cbbe12576a92990bac37a238a14c6b6fed3ab47ee1202162" }, - { url = "https://mirrors.aliyun.com/pypi/packages/9a/f3/862eaa8966c81e5525a261f199bf41e9bee890f1566211eb4647eb57dc5f/granian-2.5.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3ec26992315c00b6269d13ad31644226d5c51ae4b06e866f2a1eb2979deef482" }, - { url = "https://mirrors.aliyun.com/pypi/packages/1e/ed/aba697af1d31ca72ea060a0c1b3b214cfddbc9828615b932309b92b9c1c6/granian-2.5.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:413ce58161abbba47bde2895b0c3b6d74aec33dfd4ec355be07721b9ce57e4f2" }, - { url = "https://mirrors.aliyun.com/pypi/packages/68/81/a964fedafef328f24ee573510e632e8e1d44199dddbaab4afcb78deeb2f0/granian-2.5.5-cp314-cp314t-win_amd64.whl", hash = "sha256:8359c03c518219eacd5d60718a058d2c1fc48e2f69fcee2a022dd3814d62d81a" }, - { url = "https://mirrors.aliyun.com/pypi/packages/ca/95/de7e4ddd4e33a78ad8a3480831f99012e95b8aa9b1d0369321efde24c392/granian-2.5.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:96b2c27b02a4c75f98ff9c774d9a56092674006f7db3d349f9d792db70de830b" }, - { url = "https://mirrors.aliyun.com/pypi/packages/bd/6e/e93953739df37c8128b8fa82b1c4848abf4b1a4125423920c7e049577c7c/granian-2.5.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:c28f8cd4dfa7b37fb81a8982e4535128d629d301cdb04e4b5048b92b2ef06974" }, - { url = "https://mirrors.aliyun.com/pypi/packages/83/24/64eb5a07dcb729752a48aa1137c1d3f938c0bb20237c580e2ff4499b2204/granian-2.5.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:508545ba467ca0be870b24369901738bee6182732b9f8532baa47fe58a9db37e" }, - { url = "https://mirrors.aliyun.com/pypi/packages/42/c9/1d7e897ed39bf346d8cfc412ed17c95dbabb8e04badb6f69783460073f39/granian-2.5.5-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c504aea6cdff9e34de511eb34c85df875d17521e89493e66f20776aae272e954" }, - { url = "https://mirrors.aliyun.com/pypi/packages/2a/5f/989a250a4c1d2aeca43523bdaf0021a2e48e2e6f0c9151b9d35037fc7c4c/granian-2.5.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:99e8bf6d5fa86738fcab89d6c9f340f2084548e9af79cea835d170f90e78ed67" }, - { url = "https://mirrors.aliyun.com/pypi/packages/11/1e/55e6bff33df929d345814f7b09bff5ea283b92cf423c49d5989147fd4483/granian-2.5.5-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:717e55932ff009319fd54fa9df7a96e1be3393965219a181a916e5d8bae858a8" }, - { url = "https://mirrors.aliyun.com/pypi/packages/a9/20/e4d3b584380497251347fe50664fd38f32d9ec6a9b38e1faac65e9242104/granian-2.5.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:277402727626eaf0faed2a09676b59fb3241da4723742c65c436efeb7dd43631" }, - { url = "https://mirrors.aliyun.com/pypi/packages/f8/d1/01050fa3eb8aa8b3eafc0f6d3ea6fc506d9e3999c12106a6d59a9dc183aa/granian-2.5.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:0ea4dd78b1ca57b0176e124a5333838289fe5d0583016a646e55f54d8b4d4a14" }, - { url = "https://mirrors.aliyun.com/pypi/packages/2e/21/1c10a3f3a9e66a9b08ab7e4cf494fab6613ae050fd87ba466e2f70fb14cc/granian-2.5.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:25a9c6b406e22a0e36bb84115a92abd2fe74fb3918fce063bff8e8980ad15071" }, - { url = "https://mirrors.aliyun.com/pypi/packages/74/a5/64132e36372b9702311090acfbadae4abbaa23220d24da9f23fbd399876e/granian-2.5.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:6f78327ce9bcea86c5b14a3d58f737a6276b7c679cfd4007c1e0a42d354e97cc" }, - { url = "https://mirrors.aliyun.com/pypi/packages/4a/a0/fdc02de475d59a0bc9f00e5c7ed2c9fad4985f201ad44fd9ffa978548f4d/granian-2.5.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79f83eb500fe79b8f24b33547517937c44fd04f894a7c1c2f1cbb59892d91274" }, - { url = "https://mirrors.aliyun.com/pypi/packages/4d/10/ca4a8529ed7c4a81e166202106150d73fd2275eea7a830261eb21ee594e9/granian-2.5.5-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:4cbaf0f36016a66d1871ae83d482e633a9702a5bca53c5b0ea3120552a2c3c57" }, - { url = "https://mirrors.aliyun.com/pypi/packages/4e/6b/0fd45dbfefb78b8f9dd005d6c5179af07745cfe87d704f53657825c8326c/granian-2.5.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:47a00939040c2ad2770a24b1e8f5ad747da95b6c6602a01081209e16d5437352" }, - { url = "https://mirrors.aliyun.com/pypi/packages/36/db/c688486278c0a2cef3fd3700028eb36d4070a0ad627942745079f109b09d/granian-2.5.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a45dbabfc7ea3cbe66c90eb3715de68057719e48df33aa07f9583975ec4220f1" }, - { url = "https://mirrors.aliyun.com/pypi/packages/11/1f/ff458315d27e633701e0e1e8ed19cade40dbbc88063c4dc830a82ffa4fe2/granian-2.5.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:ea7a349a882a239512ca122b6cdd76d5dbe2de9227072a7bc3b90086b5dbd63d" }, - { url = "https://mirrors.aliyun.com/pypi/packages/b1/e0/e7bbdd9d5573c00749f24eda9de39a42321a0195f85bf2141b8d3542eb1a/granian-2.5.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:a3f8db2255b7e34c4a48c7262b97a6ecc99729e6e42578f0c6893f6761a83f2a" }, +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/db/b1/100c5add0409559ddbbecca5835c17217b7a2e026eff999bfa359a630686/granian-2.5.7.tar.gz", hash = "sha256:4702a7bcc736454803426bd2c4e7a374739ae1e4b11d27bcdc49b691d316fa0c" } +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/e1/6f/7719fc97aa081915024939f0d35fdae57dfd3d7214f7ef4a7fa664abbbc3/granian-2.5.7-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:7d84a254e9c88da874ba349f7892278a871acc391ab6af21cc32f58d27cd50a9" }, + { url = "https://mirrors.aliyun.com/pypi/packages/9f/cd/af33b780602f962c282ba3341131f7ee3b224a6c856a9fb11a017750a48f/granian-2.5.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8857d5a6ed94ea64d6b92d1d5fa8f7c1676bbecd71e6ca3d71fcd7118448af1d" }, + { url = "https://mirrors.aliyun.com/pypi/packages/6d/58/1a0d529d3d3ddc11b2b292b8f2a7566812d8691de7b1fc8ea5c8f36fd81a/granian-2.5.7-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9914dfc93f04a53a92d8cfdb059c11d620ff83e9326a99880491a9c5bc5940ef" }, + { url = "https://mirrors.aliyun.com/pypi/packages/a4/78/2a3c198ee379392d9998e4ff0cfd9ffa95b2d2c683bd15a7266a09325d43/granian-2.5.7-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:24c972fe009ca3a08fd7fb182e07fcb16bffe49c87b1c3489a6986c9e9248dc1" }, + { url = "https://mirrors.aliyun.com/pypi/packages/6e/44/7b9fba226083170e9ba221b23ab29d7ffcb761b1ef2b6ed6dac2081bc7fe/granian-2.5.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:034df207e62f104d39db479b693e03072c7eb8e202493cdf58948ff83e753cca" }, + { url = "https://mirrors.aliyun.com/pypi/packages/ff/76/f1e348991c031a50d30d3ab0625fec3b7e811092cdb0d1e996885abf1605/granian-2.5.7-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:0719052a27caca73bf4000ccdb0339a9d6705e7a4b6613b9fa88ba27c72ba659" }, + { url = "https://mirrors.aliyun.com/pypi/packages/f0/69/71b3d7d90d56fda5617fd98838ac481756ad64f76c1fc1b5e21c43a51f15/granian-2.5.7-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:be5b9224ec2583ea3b6ca90788b7f59253b6e07fcf817d14c205e6611faaf2be" }, + { url = "https://mirrors.aliyun.com/pypi/packages/74/42/603db3d0ede778adc979c6acc1eaafa5c670c795f5e0e14feb07772ed197/granian-2.5.7-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:ff246af31840369a1d06030f4d291c6a93841f68ee1f836036bce6625ae73b30" }, + { url = "https://mirrors.aliyun.com/pypi/packages/35/b5/cc557e30ba23c2934c33935768dd0233ef7a10b1e8c81dbbc63d5e2562b5/granian-2.5.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:cf79375e37a63217f9c1dc4ad15200bc5a89860b321ca30d8a5086a6ea1202e4" }, + { url = "https://mirrors.aliyun.com/pypi/packages/c3/67/ba90520cafcd13b5c76d147d713556b9eef877ca001f9ccf44d5443738b6/granian-2.5.7-cp310-cp310-win_amd64.whl", hash = "sha256:b4269a390054c0f71d9ce9d7c75ce2da0c59e78cb522016eb2f5a506c3eb6573" }, + { url = "https://mirrors.aliyun.com/pypi/packages/61/21/da3ade91b49ae99146daac6426701cc25b2c5f1413b6c8cb1cc048877036/granian-2.5.7-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:7aa90dcda1fbf03604e229465380138954d9c000eca2947a94dcfbd765414d32" }, + { url = "https://mirrors.aliyun.com/pypi/packages/76/67/a6fa402ca5ebddebec5d46dacf646ce073872e5251915a725f6abf2a23bb/granian-2.5.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:da4f27323be1188f9e325711016ee108840e14a5971bb4b4d15b65b2d1b00a2d" }, + { url = "https://mirrors.aliyun.com/pypi/packages/f9/70/accb5afd83ef785bd9e32067a13547c51cb0139076a8f2857d6d436773df/granian-2.5.7-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8ca5b7028b6ebafce30419ddb6ee7fbfb236fdd0da89427811324ddd38c7d314" }, + { url = "https://mirrors.aliyun.com/pypi/packages/74/45/98356af5f36af2b6b47a91fef0d326c275e508bf4bcf0c08bd35ed314db8/granian-2.5.7-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b83e95b18be5dfa92296bc8acfeb353488123399c90cc5f0eccf451e88bc4caf" }, + { url = "https://mirrors.aliyun.com/pypi/packages/27/7a/04d3ec13b197509c40340ec80414fbbc2b0913f6e1a18c3987cc608c8571/granian-2.5.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9aad9e920441232a7b8ad33bef7f04aae986e0e386ab7f13312477c3ea2c85df" }, + { url = "https://mirrors.aliyun.com/pypi/packages/b9/5d/1a82a596725824f6e76b8f7b853ceb464cd0334b2b8143c278aa46f23b6d/granian-2.5.7-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:777d35961d5139d203cf54d872ad5979b171e6496a471a5bcb8032f4471bdec6" }, + { url = "https://mirrors.aliyun.com/pypi/packages/94/45/b53d6d7df5cd35c3b8bb329f5ee1c7b31ead7a61a6f2046f6562028d7e1b/granian-2.5.7-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ae72c7ba1e8f35d3021dafb2ba6c4ef89f93f877218f8c6ed1cb672145cd81ad" }, + { url = "https://mirrors.aliyun.com/pypi/packages/7f/80/bb57b0fa24fcd518cd64442249459bd214ab1ec5f32590fd30389944261c/granian-2.5.7-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:3764d87edd3fddaf557dce32be396a2a56dfc5b9ad2989b1f98952983ae4a21c" }, + { url = "https://mirrors.aliyun.com/pypi/packages/7f/00/f8747aaf8dcd488e4462db89f7273dd9ae702fd17a58d72193b48eff0470/granian-2.5.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f5e21bbf1daebb0219253576cac4e5edc8fa8356ad85d66577c4f3ea2d5c6e3c" }, + { url = "https://mirrors.aliyun.com/pypi/packages/1f/69/8593d539898a870692cad447d22c2c4cc34566ad9070040ca216db6ac184/granian-2.5.7-cp311-cp311-win_amd64.whl", hash = "sha256:d210dd98852825c8a49036a6ec23cdfaa7689d1cb12ddc651c6466b412047349" }, + { url = "https://mirrors.aliyun.com/pypi/packages/b5/cf/f76d05e950f76924ffb6c5212561be4dd93fa569518869cc1233a0c77613/granian-2.5.7-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:41e3a293ac23c76d18628d1bd8376ce3230fb3afe3cf71126b8885e8da4e40c4" }, + { url = "https://mirrors.aliyun.com/pypi/packages/3f/d7/6972aa8c38d26b4cf9f35bcc9b7d3a26a3aa930e612d5913d8f4181331a1/granian-2.5.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8b345b539bcbe6dedf8a9323b0c960530cb1fb2cfb887139e6ae9513b6c04d8c" }, + { url = "https://mirrors.aliyun.com/pypi/packages/56/b4/cd5958b6af674a32296a0fef73fb499c2bf2874025062323f5dbc838f4fc/granian-2.5.7-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:12e4d7ba8e3223e2bf974860a59c29b06fa805a98ad4304be4e77180d3a28f55" }, + { url = "https://mirrors.aliyun.com/pypi/packages/7a/69/f3828de736c2802fd7fcac0bb1a0387b3332d432f0eeacb8116094926f06/granian-2.5.7-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e727d3518f038b64cb0352b34f43b387aafe5eb12b6c4b57ef598b811e40d4ed" }, + { url = "https://mirrors.aliyun.com/pypi/packages/6f/c3/b8c65cf86d473b6e99e6d985c678cb192c9b9776a966a2f4b009696bb650/granian-2.5.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59fe2b352a828a2b04bcfd105e623d66786f217759d2d6245651a7b81e4ac294" }, + { url = "https://mirrors.aliyun.com/pypi/packages/df/7e/b60421bddf187ab2a46682423e4a94b2b22a6ddff6842bf9ca2194e62ac2/granian-2.5.7-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:ec5fb593c2d436a323e711010e79718e6d5d1491d0d660fb7c9d97f7e5900830" }, + { url = "https://mirrors.aliyun.com/pypi/packages/2f/cf/3f2426e19dc955a74dc94a5a47c4170e68acb060c541ac080f71a9d55d5d/granian-2.5.7-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:48fbc25f3717d01e11547afe0e9cdf9d7c41c9f316b9623a40c22ea6b2128d36" }, + { url = "https://mirrors.aliyun.com/pypi/packages/40/2e/67e1e05ee0d503cc6e9fe53b03f69eb2f267a589d7b40873d120c417385f/granian-2.5.7-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:770935fec3374b814d21c01508c0697842d7c3750731a8ea129738b537ac594c" }, + { url = "https://mirrors.aliyun.com/pypi/packages/17/d5/9d3242bbd911434c4f3d4f14c48e73774a8ddb591e0f975eaeeaef1d5081/granian-2.5.7-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5db2600c92f74da74f624d2fdb01afe9e9365b50bd4e695a78e54961dc132f1b" }, + { url = "https://mirrors.aliyun.com/pypi/packages/10/27/b2baa0443a42d8eb59f3dfbe8186e8c80a090655584af4611f22f1592d7a/granian-2.5.7-cp312-cp312-win_amd64.whl", hash = "sha256:bc368bdeb21646a965adf9f43dd2f4a770647e50318ba1b7cf387d4916ed7e69" }, + { url = "https://mirrors.aliyun.com/pypi/packages/54/ec/bf1b7eefe824630d1d3ae9a8af397d823f2339d3adec71e9ee49d667409c/granian-2.5.7-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:fafb9c17def635bb0a5e20e145601598a6767b879bc2501663dbb45a57d1bc2e" }, + { url = "https://mirrors.aliyun.com/pypi/packages/28/f7/5172daf1968c3a2337c51c50f4a3013aaab564d012d3a79e8390cc66403b/granian-2.5.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9616a197eba637d59242661be8a46127c3f79f7c9bbfa44c0ea8c8c790a11d5e" }, + { url = "https://mirrors.aliyun.com/pypi/packages/92/10/4344ccacc3f8dea973d630306491de43fbd4a0248e3f7cc9ff09ed5cc524/granian-2.5.7-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cfd7a09d5eb00a271ec79e3e0bbf069aa62ce376b64825bdeacb668d2b2a4041" }, + { url = "https://mirrors.aliyun.com/pypi/packages/5e/33/638cf8c7f23ab905d3f6a371b5f87d03fd611678424223a0f1d0f7766cc7/granian-2.5.7-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1438a82264690fce6e82de66a95c77f5b0a5c33b93269eb85fc69ce0112c12d5" }, + { url = "https://mirrors.aliyun.com/pypi/packages/18/42/6ec25d37ffc1f08679e6b325e9f9ac199ba5def948904c9205cd34fbfe6b/granian-2.5.7-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3573121da77aac1af64cf90a88f29b2daecbf92458beec187421a382039f366" }, + { url = "https://mirrors.aliyun.com/pypi/packages/b0/1e/db85dac58d84d3e50e427fe5b60b4f8e8a561d9784971fa3b2879198ad88/granian-2.5.7-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:34cdb82024efbcc9de01c7505213be17e4ba5e7a3acabe74ecd93ba31de7673e" }, + { url = "https://mirrors.aliyun.com/pypi/packages/d9/25/a38fd12e1661bbd8535203a8b61240feac7b6b96726bff4de23b0078ab9f/granian-2.5.7-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:572451e94de69df228e4314cb91a50dee1565c4a53d33ffac5936c6ec9c5aba2" }, + { url = "https://mirrors.aliyun.com/pypi/packages/d3/cd/852913a0fc30efc24495453c0f973dd74ef13aa0561afb352afa4b6ecbc2/granian-2.5.7-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6e1679a4b102511b483774397134d244108851ae7a1e8bef09a8ef927ab4d370" }, + { url = "https://mirrors.aliyun.com/pypi/packages/60/64/0dff100ce1e43c700918b39656cc000b1163c144eac3a12563a5f692dcd1/granian-2.5.7-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:285be70dcf3c70121afec03e691596db94bd786f9bebc229e9e0319686857d82" }, + { url = "https://mirrors.aliyun.com/pypi/packages/19/ab/e66cf9bf57800dd7c2a2a4b8f23124603fce561a65a176f4cf3794a85b92/granian-2.5.7-cp313-cp313-win_amd64.whl", hash = "sha256:1273c9b1d38d19bcdd550a9a846d07112e541cfa1f99be04fbb926f2a003df3d" }, + { url = "https://mirrors.aliyun.com/pypi/packages/da/0e/feca4a20e7b9e7de0e58103278c6581ebf3d5c1b972ed1c2dcfd25741f15/granian-2.5.7-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:75b9798bc13baa76e35165e5a778cd58a7258d5a2112ed6ef84ef84874244856" }, + { url = "https://mirrors.aliyun.com/pypi/packages/f7/fe/65ca38ba9b9f4805495d96ed7b774dfd300f7c944f088db39c676c16501e/granian-2.5.7-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4cb8247728680ca308b7dc41a6d27582b78e15e902377e89000711f1126524dd" }, + { url = "https://mirrors.aliyun.com/pypi/packages/75/d1/b9dea32fbafabe5c7b049fb0209149a37c6b8468c698d066448cbe88dc85/granian-2.5.7-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64348b83f1ad2f7a29df7932dc518ad669cb61a08a9cde02ca8ede8e9b110506" }, + { url = "https://mirrors.aliyun.com/pypi/packages/cb/9e/d29485ab18896e4d911e33b006af7a9b7098316a78938d6b7455c523fea5/granian-2.5.7-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:e2292d4a4661c79d471fa0ff6fe640018c923b6a6dd1bb5383b368b3d5ec2a0c" }, + { url = "https://mirrors.aliyun.com/pypi/packages/41/cd/58c67dc191caeecbbb15ee39d433136dd064c13778b4551661bd902b5a78/granian-2.5.7-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:45903d2f2f88a9cd4a7d0b8ec329db1fb2d9e15bf38153087a3b217b9cdb0046" }, + { url = "https://mirrors.aliyun.com/pypi/packages/16/0b/04e4977df3ef7607a8b6625caed7cac107a049120d2452c33392d4544875/granian-2.5.7-cp313-cp313t-musllinux_1_1_armv7l.whl", hash = "sha256:106e8988e42e527c18b763be5faae7e8f602caac6cb93657793638fc9ab41c98" }, + { url = "https://mirrors.aliyun.com/pypi/packages/c7/89/4e10e18fc107e5929143a06d9257646963cf5621c928b3d2774e5a85652a/granian-2.5.7-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:711632e602c4ea08b827bf6095c2c6fbe6005c7a05f142ae2b4d9e1d45cefbd9" }, + { url = "https://mirrors.aliyun.com/pypi/packages/57/81/94e416056d8b4b1cd09cc8065a1e240b0af99f21301c209571530cd83dd0/granian-2.5.7-cp313-cp313t-win_amd64.whl", hash = "sha256:1c571733aa0fdb6755be9ffb3cd728ef965ae565ba896e407d6019bad929d7bb" }, + { url = "https://mirrors.aliyun.com/pypi/packages/63/89/207ebcbd084ed992ecb3739376fd292e6a5bf6ae80b35f06e4f382e1f193/granian-2.5.7-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:74ad35feeafc12efdc27d59a393f8b95235095c4e46c8b8dd6d50ee9e928118d" }, + { url = "https://mirrors.aliyun.com/pypi/packages/8a/4b/f941c645d5e3ab495f0cb056abebdb16fb761f713c35a830521f4531674b/granian-2.5.7-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:875f5cc36b960039bfc99a37af32ad98b3abe753a6de92a9f91268c16bfeb192" }, + { url = "https://mirrors.aliyun.com/pypi/packages/de/14/af9bbf26389f6d0cbdd7445cc969da50965363b2c9635acdae08eb4f2d9b/granian-2.5.7-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:478123ee817f742a6f67050ae4de46bc807c874e397a379cf9fb9ed68b66d7ad" }, + { url = "https://mirrors.aliyun.com/pypi/packages/08/0e/4fa5d4317ff88eab5d061cb45339fdf09a044ae9c7b2496b81c2de5bc2c6/granian-2.5.7-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7d0d0530960250ac9b78494999f2687c627ac5060013e4c63856afb493c2518" }, + { url = "https://mirrors.aliyun.com/pypi/packages/0c/05/977fcfe66c9ecd72da47e5185bcd78150efcb5d3bca1ba77860fe8f7bad7/granian-2.5.7-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50cf8cb02253bfc42ee1bb6c5912507f83bea0a39c3d8a09988939407e08787b" }, + { url = "https://mirrors.aliyun.com/pypi/packages/01/c0/fd4d0b455d34c493cfbc6f450e0005206ab41a68f65f16f89e9ae84669ed/granian-2.5.7-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:78015fcb4d055e0eb2454d07f167ca2aa9f48609f90484750b99ca9b719701c4" }, + { url = "https://mirrors.aliyun.com/pypi/packages/ce/55/13d53add16a349b5c9384afac14b519a54b7fa4bf73540338296f0963ee7/granian-2.5.7-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:bd254e68cc8471b725aa6610b68a5e004aa92b8db53c0d01c408bef8bc9cdcb4" }, + { url = "https://mirrors.aliyun.com/pypi/packages/24/b3/addad51cef2472105b664b608a2b8eccc5691d08c532862cd21b52023661/granian-2.5.7-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:028480ddef683df00064664e7bf58358650722dfa40c2a6dcbf50b3d1996dbb0" }, + { url = "https://mirrors.aliyun.com/pypi/packages/d3/2c/ceab57671c7ade9305ed9e86471507b7721e92435509bb3ecab7e1c28fa8/granian-2.5.7-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:b42a254b2884b3060dcafc49dee477f3f6e8c63c567f179dbec7853d6739f124" }, + { url = "https://mirrors.aliyun.com/pypi/packages/a7/5b/5458d995ed5a1fe4a7aa1e2587f550c00ec80d373531e270080e4d5e1ca5/granian-2.5.7-cp314-cp314-win_amd64.whl", hash = "sha256:8f6466077c76d92f8926885280166e6874640bbab11ce10c4a3b04c0ee182ac6" }, + { url = "https://mirrors.aliyun.com/pypi/packages/e2/6d/3c6fdf84e9de25e0023302d5efd98d70fd6147cae98453591a317539bba6/granian-2.5.7-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:6fc06ac1c147e2f01639aa5c7c0f9553f8c6b283665d13d5527a051e917db150" }, + { url = "https://mirrors.aliyun.com/pypi/packages/23/92/3fc35058908d1ecb3cb556de729e6f5853e888ac7022a141885f6a3079a5/granian-2.5.7-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:dec92e09f512aaf532bb75de69b858958113efe52b16a9c5ef19d64063b4956c" }, + { url = "https://mirrors.aliyun.com/pypi/packages/76/82/3fc67aa247dcac09c948ae8a3dc02568d4eb8135f9938594ee5d2ba25a4f/granian-2.5.7-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:01e18c9c63b89370e42d65bc4eccec349d0b676ee69ccbcbbf9bedf606ded129" }, + { url = "https://mirrors.aliyun.com/pypi/packages/97/4c/11f293a60892df7cfdcbb1648ddc31e9d4471b52843e4e838a2a58773fff/granian-2.5.7-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:035e3b145827a12fb25de5b5122a11d9dad93a943e2251d83ee593b28b0397dc" }, + { url = "https://mirrors.aliyun.com/pypi/packages/42/a0/d4f0063938431201fc7884c7e7bfc5488e3de09957cce37090af9131b7f4/granian-2.5.7-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:21278e2862d7e52996b03260a2a65288c731474c71a6d8311ef78025696b883d" }, + { url = "https://mirrors.aliyun.com/pypi/packages/e6/85/327e15e9e96eb35fcca3fbd9848df6bc180f7fb04c9116e22d3c10ada98e/granian-2.5.7-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:fd6a7645117034753ec91e667316e93f3d0325f79462979af3e2e316278ae235" }, + { url = "https://mirrors.aliyun.com/pypi/packages/78/5c/67224ee8fa71ee3748d931c34cf6f85e30c77b2a3ac0b1ca70c640b37d10/granian-2.5.7-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:133d3453d29c5a22648c879d078d097a4ea74b8f84c530084c32debdfdd9d5fd" }, + { url = "https://mirrors.aliyun.com/pypi/packages/45/e0/df08a75311c8d9505dc4f381a4a21bbfeed58b8c8f6d7c3a34b049ad9c34/granian-2.5.7-cp314-cp314t-win_amd64.whl", hash = "sha256:ab8f0f4f22d2efcce194f5b1d66beef2ba3d4bcd18f9afd6b749afa48fdb9a7d" }, + { url = "https://mirrors.aliyun.com/pypi/packages/0e/25/2a4112983df5ce0ec8407121ad72c17d27ebfad57085749b8e4164d69e63/granian-2.5.7-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:cdae1c86357bfe895ffd0065c0403913bc008f752e2f77ab363d4e3b4276009b" }, + { url = "https://mirrors.aliyun.com/pypi/packages/d7/0a/eb0c5b71355e8f99b89dc335f16cd5108763c554e96a2aae5e7162ef4997/granian-2.5.7-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:bc1d8aaf5bfc5fc9f8f590a42e9f88a43d19ad71f670c6969fa791b52ce1f5ec" }, + { url = "https://mirrors.aliyun.com/pypi/packages/f2/9c/4c592c5a813a921033a37a0f003278b1f772a6c9abd16f821bcb119151f0/granian-2.5.7-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:288b62c19aea5b162d27e229469b6307a78cb272aa8fcc296dbfca9fbbda4d8f" }, + { url = "https://mirrors.aliyun.com/pypi/packages/f1/35/96af9f0995a7c45f0cd31261ab6284e5d6028afa17c6fcfe757cccb0afb5/granian-2.5.7-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:66c3d2619dc5e845d658cf3ed4f7370f83d5323a85ff8338e7c7a27d9a333841" }, + { url = "https://mirrors.aliyun.com/pypi/packages/fc/93/45c253983c2001f534ba2c7bc1e53718fc8cecf196b1e1a0469d5874ae54/granian-2.5.7-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:323e35d5d5054d2568fc824798471e7d33314f47aebd556c4fbf4894e539347d" }, + { url = "https://mirrors.aliyun.com/pypi/packages/25/77/c03e60c7bed386ab16cf15b317dea7f95dde5095af6e17cbd657cd82c21b/granian-2.5.7-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:026ef2588a2b991b250768bf47538fd5fd864549535f885239b6908b214299c4" }, + { url = "https://mirrors.aliyun.com/pypi/packages/5e/c9/2bce3db4e3da8d3a697c363c8f699b71f05b7f7a0458e1ba345eaea53fcd/granian-2.5.7-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:4717a62c0a1b79372c495b99ade18bfc3c4a365242bf75770c96a4767a9bcf66" }, + { url = "https://mirrors.aliyun.com/pypi/packages/78/66/997ebfd8cc4a0640befb970bc846a76437d1f0b55dff179e69f29fa4615b/granian-2.5.7-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:4b57ae0a2e1dbc7a248e3c08440b490b3f247e7e4f997faa72e82f5a89d0ea4c" }, + { url = "https://mirrors.aliyun.com/pypi/packages/16/0f/da2588ac78254a4d0be90a6f733d0bb7dd1edb78a10d9e59fa9837687e94/granian-2.5.7-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:bee545c9b9e38eabcdd675e3fec1a2112b8193dc864739952b9de8131433a31c" }, + { url = "https://mirrors.aliyun.com/pypi/packages/7d/34/75def8343534e9d48362c43c3cbd06242a2d7804fbfbc824c8aa9fb75a30/granian-2.5.7-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:73c76c0f1ee46506224e92df193b4d271ea89f0d82cd69301784ca85bc1db515" }, + { url = "https://mirrors.aliyun.com/pypi/packages/c3/5d/d828d97aad050cfc5b18a0163b532c289a35ad214e31f5a129695b2b4cae/granian-2.5.7-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68879c27aed972f647a8e8ef37f9046f71d7507dc9b3ceffa97d2fbffe6a16c8" }, + { url = "https://mirrors.aliyun.com/pypi/packages/2d/57/b8380f3d6b6dcdcd454d720cf11dbecb0e2071a870f44eb834011f14b573/granian-2.5.7-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:ea9cbdfbd750813866dcc9c020018e5f20a57a4e3a83bd049ccc1f6da0559b75" }, + { url = "https://mirrors.aliyun.com/pypi/packages/0b/e9/04a7c3b83650afc4a4ad82b67e6306d99f80ac1a6aacb3a8ba182f7359d6/granian-2.5.7-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:d142ff5ee6027515370e56f95d179ec3e81bd265d5b4958de2b19adcdf34887d" }, + { url = "https://mirrors.aliyun.com/pypi/packages/2b/bf/a1cdbff73cbac4fddf817d06c13ce6cdc75c22d6da1b257e3563fea4c3c5/granian-2.5.7-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:222f0fb1688a62ca23cb3da974cefa69e7fdc40fd548d1ae87a953225e1d1cbb" }, + { url = "https://mirrors.aliyun.com/pypi/packages/c8/cc/35c6a55ac2c211e86a9f0c728eb81b6ad19f05a3055d79c6f11a1b71f5d5/granian-2.5.7-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:40494c6cda1ad881ae07efbb2dc4a1ca8f12d5c6cf28d1ab8b0f2db13826617b" }, + { url = "https://mirrors.aliyun.com/pypi/packages/f3/0a/5a95a3889532bc5a5f652cdc78dae8ffa16d4228b4d35256a98be89e33ef/granian-2.5.7-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c3942d08af2c8b67d0ef569b6c567284433ebf09b4af3ea68388abb7caccad2b" }, ] [[package]] @@ -1017,6 +1017,8 @@ wheels = [ { url = "https://mirrors.aliyun.com/pypi/packages/7f/91/ae2eb6b7979e2f9b035a9f612cf70f1bf54aad4e1d125129bef1eae96f19/greenlet-3.2.4-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c2ca18a03a8cfb5b25bc1cbe20f3d9a4c80d8c3b13ba3df49ac3961af0b1018d" }, { url = "https://mirrors.aliyun.com/pypi/packages/f7/85/433de0c9c0252b22b16d413c9407e6cb3b41df7389afc366ca204dbc1393/greenlet-3.2.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9fe0a28a7b952a21e2c062cd5756d34354117796c6d9215a87f55e38d15402c5" }, { url = "https://mirrors.aliyun.com/pypi/packages/a1/8d/88f3ebd2bc96bf7747093696f4335a0a8a4c5acfcf1b757717c0d2474ba3/greenlet-3.2.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8854167e06950ca75b898b104b63cc646573aa5fef1353d4508ecdd1ee76254f" }, + { url = "https://mirrors.aliyun.com/pypi/packages/f1/29/74242b7d72385e29bcc5563fba67dad94943d7cd03552bac320d597f29b2/greenlet-3.2.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f47617f698838ba98f4ff4189aef02e7343952df3a615f847bb575c3feb177a7" }, + { url = "https://mirrors.aliyun.com/pypi/packages/c8/e2/1572b8eeab0f77df5f6729d6ab6b141e4a84ee8eb9bc8c1e7918f94eda6d/greenlet-3.2.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:af41be48a4f60429d5cad9d22175217805098a9ef7c40bfef44f7669fb9d74d8" }, { url = "https://mirrors.aliyun.com/pypi/packages/d6/6f/b60b0291d9623c496638c582297ead61f43c4b72eef5e9c926ef4565ec13/greenlet-3.2.4-cp310-cp310-win_amd64.whl", hash = "sha256:73f49b5368b5359d04e18d15828eecc1806033db5233397748f4ca813ff1056c" }, { url = "https://mirrors.aliyun.com/pypi/packages/a4/de/f28ced0a67749cac23fecb02b694f6473f47686dff6afaa211d186e2ef9c/greenlet-3.2.4-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:96378df1de302bc38e99c3a9aa311967b7dc80ced1dcc6f171e99842987882a2" }, { url = "https://mirrors.aliyun.com/pypi/packages/09/16/2c3792cba130000bf2a31c5272999113f4764fd9d874fb257ff588ac779a/greenlet-3.2.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1ee8fae0519a337f2329cb78bd7a8e128ec0f881073d43f023c7b8d4831d5246" }, @@ -1026,6 +1028,8 @@ wheels = [ { url = "https://mirrors.aliyun.com/pypi/packages/1f/8e/abdd3f14d735b2929290a018ecf133c901be4874b858dd1c604b9319f064/greenlet-3.2.4-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2523e5246274f54fdadbce8494458a2ebdcdbc7b802318466ac5606d3cded1f8" }, { url = "https://mirrors.aliyun.com/pypi/packages/5d/65/deb2a69c3e5996439b0176f6651e0052542bb6c8f8ec2e3fba97c9768805/greenlet-3.2.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1987de92fec508535687fb807a5cea1560f6196285a4cde35c100b8cd632cc52" }, { url = "https://mirrors.aliyun.com/pypi/packages/3f/cc/b07000438a29ac5cfb2194bfc128151d52f333cee74dd7dfe3fb733fc16c/greenlet-3.2.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:55e9c5affaa6775e2c6b67659f3a71684de4c549b3dd9afca3bc773533d284fa" }, + { url = "https://mirrors.aliyun.com/pypi/packages/67/24/28a5b2fa42d12b3d7e5614145f0bd89714c34c08be6aabe39c14dd52db34/greenlet-3.2.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c9c6de1940a7d828635fbd254d69db79e54619f165ee7ce32fda763a9cb6a58c" }, + { url = "https://mirrors.aliyun.com/pypi/packages/6a/05/03f2f0bdd0b0ff9a4f7b99333d57b53a7709c27723ec8123056b084e69cd/greenlet-3.2.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:03c5136e7be905045160b1b9fdca93dd6727b180feeafda6818e6496434ed8c5" }, { url = "https://mirrors.aliyun.com/pypi/packages/d8/0f/30aef242fcab550b0b3520b8e3561156857c94288f0332a79928c31a52cf/greenlet-3.2.4-cp311-cp311-win_amd64.whl", hash = "sha256:9c40adce87eaa9ddb593ccb0fa6a07caf34015a29bf8d344811665b573138db9" }, { url = "https://mirrors.aliyun.com/pypi/packages/44/69/9b804adb5fd0671f367781560eb5eb586c4d495277c93bde4307b9e28068/greenlet-3.2.4-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:3b67ca49f54cede0186854a008109d6ee71f66bd57bb36abd6d0a0267b540cdd" }, { url = "https://mirrors.aliyun.com/pypi/packages/46/e9/d2a80c99f19a153eff70bc451ab78615583b8dac0754cfb942223d2c1a0d/greenlet-3.2.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ddf9164e7a5b08e9d22511526865780a576f19ddd00d62f8a665949327fde8bb" }, @@ -1035,6 +1039,8 @@ wheels = [ { url = "https://mirrors.aliyun.com/pypi/packages/19/0d/6660d55f7373b2ff8152401a83e02084956da23ae58cddbfb0b330978fe9/greenlet-3.2.4-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b3812d8d0c9579967815af437d96623f45c0f2ae5f04e366de62a12d83a8fb0" }, { url = "https://mirrors.aliyun.com/pypi/packages/8e/1a/c953fdedd22d81ee4629afbb38d2f9d71e37d23caace44775a3a969147d4/greenlet-3.2.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:abbf57b5a870d30c4675928c37278493044d7c14378350b3aa5d484fa65575f0" }, { url = "https://mirrors.aliyun.com/pypi/packages/3f/c7/12381b18e21aef2c6bd3a636da1088b888b97b7a0362fac2e4de92405f97/greenlet-3.2.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:20fb936b4652b6e307b8f347665e2c615540d4b42b3b4c8a321d8286da7e520f" }, + { url = "https://mirrors.aliyun.com/pypi/packages/27/45/80935968b53cfd3f33cf99ea5f08227f2646e044568c9b1555b58ffd61c2/greenlet-3.2.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ee7a6ec486883397d70eec05059353b8e83eca9168b9f3f9a361971e77e0bcd0" }, + { url = "https://mirrors.aliyun.com/pypi/packages/69/02/b7c30e5e04752cb4db6202a3858b149c0710e5453b71a3b2aec5d78a1aab/greenlet-3.2.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:326d234cbf337c9c3def0676412eb7040a35a768efc92504b947b3e9cfc7543d" }, { url = "https://mirrors.aliyun.com/pypi/packages/e9/08/b0814846b79399e585f974bbeebf5580fbe59e258ea7be64d9dfb253c84f/greenlet-3.2.4-cp312-cp312-win_amd64.whl", hash = "sha256:a7d4e128405eea3814a12cc2605e0e6aedb4035bf32697f72deca74de4105e02" }, { url = "https://mirrors.aliyun.com/pypi/packages/49/e8/58c7f85958bda41dafea50497cbd59738c5c43dbbea5ee83d651234398f4/greenlet-3.2.4-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:1a921e542453fe531144e91e1feedf12e07351b1cf6c9e8a3325ea600a715a31" }, { url = "https://mirrors.aliyun.com/pypi/packages/62/dd/b9f59862e9e257a16e4e610480cfffd29e3fae018a68c2332090b53aac3d/greenlet-3.2.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd3c8e693bff0fff6ba55f140bf390fa92c994083f838fece0f63be121334945" }, @@ -1044,6 +1050,8 @@ wheels = [ { url = "https://mirrors.aliyun.com/pypi/packages/ee/43/3cecdc0349359e1a527cbf2e3e28e5f8f06d3343aaf82ca13437a9aa290f/greenlet-3.2.4-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23768528f2911bcd7e475210822ffb5254ed10d71f4028387e5a99b4c6699671" }, { url = "https://mirrors.aliyun.com/pypi/packages/b8/19/06b6cf5d604e2c382a6f31cafafd6f33d5dea706f4db7bdab184bad2b21d/greenlet-3.2.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:00fadb3fedccc447f517ee0d3fd8fe49eae949e1cd0f6a611818f4f6fb7dc83b" }, { url = "https://mirrors.aliyun.com/pypi/packages/a2/15/0d5e4e1a66fab130d98168fe984c509249c833c1a3c16806b90f253ce7b9/greenlet-3.2.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:d25c5091190f2dc0eaa3f950252122edbbadbb682aa7b1ef2f8af0f8c0afefae" }, + { url = "https://mirrors.aliyun.com/pypi/packages/1c/53/f9c440463b3057485b8594d7a638bed53ba531165ef0ca0e6c364b5cc807/greenlet-3.2.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6e343822feb58ac4d0a1211bd9399de2b3a04963ddeec21530fc426cc121f19b" }, + { url = "https://mirrors.aliyun.com/pypi/packages/47/e4/3bb4240abdd0a8d23f4f88adec746a3099f0d86bfedb623f063b2e3b4df0/greenlet-3.2.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ca7f6f1f2649b89ce02f6f229d7c19f680a6238af656f61e0115b24857917929" }, { url = "https://mirrors.aliyun.com/pypi/packages/0b/55/2321e43595e6801e105fcfdee02b34c0f996eb71e6ddffca6b10b7e1d771/greenlet-3.2.4-cp313-cp313-win_amd64.whl", hash = "sha256:554b03b6e73aaabec3745364d6239e9e012d64c68ccd0b8430c64ccc14939a8b" }, { url = "https://mirrors.aliyun.com/pypi/packages/22/5c/85273fd7cc388285632b0498dbbab97596e04b154933dfe0f3e68156c68c/greenlet-3.2.4-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:49a30d5fda2507ae77be16479bdb62a660fa51b1eb4928b524975b3bde77b3c0" }, { url = "https://mirrors.aliyun.com/pypi/packages/d1/75/10aeeaa3da9332c2e761e4c50d4c3556c21113ee3f0afa2cf5769946f7a3/greenlet-3.2.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:299fd615cd8fc86267b47597123e3f43ad79c9d8a22bebdce535e53550763e2f" }, @@ -1051,6 +1059,8 @@ wheels = [ { url = "https://mirrors.aliyun.com/pypi/packages/dc/8b/29aae55436521f1d6f8ff4e12fb676f3400de7fcf27fccd1d4d17fd8fecd/greenlet-3.2.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b4a1870c51720687af7fa3e7cda6d08d801dae660f75a76f3845b642b4da6ee1" }, { url = "https://mirrors.aliyun.com/pypi/packages/92/2e/ea25914b1ebfde93b6fc4ff46d6864564fba59024e928bdc7de475affc25/greenlet-3.2.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:061dc4cf2c34852b052a8620d40f36324554bc192be474b9e9770e8c042fd735" }, { url = "https://mirrors.aliyun.com/pypi/packages/72/60/fc56c62046ec17f6b0d3060564562c64c862948c9d4bc8aa807cf5bd74f4/greenlet-3.2.4-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44358b9bf66c8576a9f57a590d5f5d6e72fa4228b763d0e43fee6d3b06d3a337" }, + { url = "https://mirrors.aliyun.com/pypi/packages/23/6e/74407aed965a4ab6ddd93a7ded3180b730d281c77b765788419484cdfeef/greenlet-3.2.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2917bdf657f5859fbf3386b12d68ede4cf1f04c90c3a6bc1f013dd68a22e2269" }, + { url = "https://mirrors.aliyun.com/pypi/packages/0d/da/343cd760ab2f92bac1845ca07ee3faea9fe52bee65f7bcb19f16ad7de08b/greenlet-3.2.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:015d48959d4add5d6c9f6c5210ee3803a830dce46356e3bc326d6776bde54681" }, { url = "https://mirrors.aliyun.com/pypi/packages/e3/a5/6ddab2b4c112be95601c13428db1d8b6608a8b6039816f2ba09c346c08fc/greenlet-3.2.4-cp314-cp314-win_amd64.whl", hash = "sha256:e37ab26028f12dbb0ff65f29a8d3d44a765c61e729647bf2ddfbbed621726f01" }, ] @@ -1902,28 +1912,28 @@ wheels = [ [[package]] name = "psutil" -version = "7.1.2" -source = { registry = "https://mirrors.aliyun.com/pypi/simple" } -sdist = { url = "https://mirrors.aliyun.com/pypi/packages/cd/ec/7b8e6b9b1d22708138630ef34c53ab2b61032c04f16adfdbb96791c8c70c/psutil-7.1.2.tar.gz", hash = "sha256:aa225cdde1335ff9684708ee8c72650f6598d5ed2114b9a7c5802030b1785018" } -wheels = [ - { url = "https://mirrors.aliyun.com/pypi/packages/b8/d9/b56cc9f883140ac10021a8c9b0f4e16eed1ba675c22513cdcbce3ba64014/psutil-7.1.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0cc5c6889b9871f231ed5455a9a02149e388fffcb30b607fb7a8896a6d95f22e" }, - { url = "https://mirrors.aliyun.com/pypi/packages/36/eb/28d22de383888deb252c818622196e709da98816e296ef95afda33f1c0a2/psutil-7.1.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8e9e77a977208d84aa363a4a12e0f72189d58bbf4e46b49aae29a2c6e93ef206" }, - { url = "https://mirrors.aliyun.com/pypi/packages/89/5d/220039e2f28cc129626e54d63892ab05c0d56a29818bfe7268dcb5008932/psutil-7.1.2-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7d9623a5e4164d2220ecceb071f4b333b3c78866141e8887c072129185f41278" }, - { url = "https://mirrors.aliyun.com/pypi/packages/ba/7a/286f0e1c167445b2ef4a6cbdfc8c59fdb45a5a493788950cf8467201dc73/psutil-7.1.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:364b1c10fe4ed59c89ec49e5f1a70da353b27986fa8233b4b999df4742a5ee2f" }, - { url = "https://mirrors.aliyun.com/pypi/packages/aa/cc/7eb93260794a42e39b976f3a4dde89725800b9f573b014fac142002a5c98/psutil-7.1.2-cp313-cp313t-win_amd64.whl", hash = "sha256:f101ef84de7e05d41310e3ccbdd65a6dd1d9eed85e8aaf0758405d022308e204" }, - { url = "https://mirrors.aliyun.com/pypi/packages/ab/1a/0681a92b53366e01f0a099f5237d0c8a2f79d322ac589cccde5e30c8a4e2/psutil-7.1.2-cp313-cp313t-win_arm64.whl", hash = "sha256:20c00824048a95de67f00afedc7b08b282aa08638585b0206a9fb51f28f1a165" }, - { url = "https://mirrors.aliyun.com/pypi/packages/56/9e/f1c5c746b4ed5320952acd3002d3962fe36f30524c00ea79fdf954cc6779/psutil-7.1.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:e09cfe92aa8e22b1ec5e2d394820cf86c5dff6367ac3242366485dfa874d43bc" }, - { url = "https://mirrors.aliyun.com/pypi/packages/32/ee/fd26216a735395cc25c3899634e34aeb41fb1f3dbb44acc67d9e594be562/psutil-7.1.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:fa6342cf859c48b19df3e4aa170e4cfb64aadc50b11e06bb569c6c777b089c9e" }, - { url = "https://mirrors.aliyun.com/pypi/packages/3c/cd/7d96eaec4ef7742b845a9ce2759a2769ecce4ab7a99133da24abacbc9e41/psutil-7.1.2-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:625977443498ee7d6c1e63e93bacca893fd759a66c5f635d05e05811d23fb5ee" }, - { url = "https://mirrors.aliyun.com/pypi/packages/bc/1a/7f0b84bdb067d35fe7fade5fff888408688caf989806ce2d6dae08c72dd5/psutil-7.1.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a24bcd7b7f2918d934af0fb91859f621b873d6aa81267575e3655cd387572a7" }, - { url = "https://mirrors.aliyun.com/pypi/packages/de/05/7820ef8f7b275268917e0c750eada5834581206d9024ca88edce93c4b762/psutil-7.1.2-cp314-cp314t-win_amd64.whl", hash = "sha256:329f05610da6380982e6078b9d0881d9ab1e9a7eb7c02d833bfb7340aa634e31" }, - { url = "https://mirrors.aliyun.com/pypi/packages/db/9a/58de399c7cb58489f08498459ff096cd76b3f1ddc4f224ec2c5ef729c7d0/psutil-7.1.2-cp314-cp314t-win_arm64.whl", hash = "sha256:7b04c29e3c0c888e83ed4762b70f31e65c42673ea956cefa8ced0e31e185f582" }, - { url = "https://mirrors.aliyun.com/pypi/packages/ae/89/b9f8d47ddbc52d7301fc868e8224e5f44ed3c7f55e6d0f54ecaf5dd9ff5e/psutil-7.1.2-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:c9ba5c19f2d46203ee8c152c7b01df6eec87d883cfd8ee1af2ef2727f6b0f814" }, - { url = "https://mirrors.aliyun.com/pypi/packages/c8/7a/8628c2f6b240680a67d73d8742bb9ff39b1820a693740e43096d5dcb01e5/psutil-7.1.2-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:2a486030d2fe81bec023f703d3d155f4823a10a47c36784c84f1cc7f8d39bedb" }, - { url = "https://mirrors.aliyun.com/pypi/packages/30/28/5e27f4d5a0e347f8e3cc16cd7d35533dbce086c95807f1f0e9cd77e26c10/psutil-7.1.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3efd8fc791492e7808a51cb2b94889db7578bfaea22df931424f874468e389e3" }, - { url = "https://mirrors.aliyun.com/pypi/packages/e5/5c/79cf60c9acf36d087f0db0f82066fca4a780e97e5b3a2e4c38209c03d170/psutil-7.1.2-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e2aeb9b64f481b8eabfc633bd39e0016d4d8bbcd590d984af764d80bf0851b8a" }, - { url = "https://mirrors.aliyun.com/pypi/packages/f7/03/0a464404c51685dcb9329fdd660b1721e076ccd7b3d97dee066bcc9ffb15/psutil-7.1.2-cp37-abi3-win_amd64.whl", hash = "sha256:8e17852114c4e7996fe9da4745c2bdef001ebbf2f260dec406290e66628bdb91" }, - { url = "https://mirrors.aliyun.com/pypi/packages/6a/32/97ca2090f2f1b45b01b6aa7ae161cfe50671de097311975ca6eea3e7aabc/psutil-7.1.2-cp37-abi3-win_arm64.whl", hash = "sha256:3e988455e61c240cc879cb62a008c2699231bf3e3d061d7fce4234463fd2abb4" }, +version = "7.1.3" +source = { registry = "https://mirrors.aliyun.com/pypi/simple" } +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/e1/88/bdd0a41e5857d5d703287598cbf08dad90aed56774ea52ae071bae9071b6/psutil-7.1.3.tar.gz", hash = "sha256:6c86281738d77335af7aec228328e944b30930899ea760ecf33a4dba66be5e74" } +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/bd/93/0c49e776b8734fef56ec9c5c57f923922f2cf0497d62e0f419465f28f3d0/psutil-7.1.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0005da714eee687b4b8decd3d6cc7c6db36215c9e74e5ad2264b90c3df7d92dc" }, + { url = "https://mirrors.aliyun.com/pypi/packages/6f/8d/b31e39c769e70780f007969815195a55c81a63efebdd4dbe9e7a113adb2f/psutil-7.1.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:19644c85dcb987e35eeeaefdc3915d059dac7bd1167cdcdbf27e0ce2df0c08c0" }, + { url = "https://mirrors.aliyun.com/pypi/packages/62/61/23fd4acc3c9eebbf6b6c78bcd89e5d020cfde4acf0a9233e9d4e3fa698b4/psutil-7.1.3-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95ef04cf2e5ba0ab9eaafc4a11eaae91b44f4ef5541acd2ee91d9108d00d59a7" }, + { url = "https://mirrors.aliyun.com/pypi/packages/30/1c/f921a009ea9ceb51aa355cb0cc118f68d354db36eae18174bab63affb3e6/psutil-7.1.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1068c303be3a72f8e18e412c5b2a8f6d31750fb152f9cb106b54090296c9d251" }, + { url = "https://mirrors.aliyun.com/pypi/packages/a6/82/62d68066e13e46a5116df187d319d1724b3f437ddd0f958756fc052677f4/psutil-7.1.3-cp313-cp313t-win_amd64.whl", hash = "sha256:18349c5c24b06ac5612c0428ec2a0331c26443d259e2a0144a9b24b4395b58fa" }, + { url = "https://mirrors.aliyun.com/pypi/packages/df/ad/c1cd5fe965c14a0392112f68362cfceb5230819dbb5b1888950d18a11d9f/psutil-7.1.3-cp313-cp313t-win_arm64.whl", hash = "sha256:c525ffa774fe4496282fb0b1187725793de3e7c6b29e41562733cae9ada151ee" }, + { url = "https://mirrors.aliyun.com/pypi/packages/2e/bb/6670bded3e3236eb4287c7bcdc167e9fae6e1e9286e437f7111caed2f909/psutil-7.1.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b403da1df4d6d43973dc004d19cee3b848e998ae3154cc8097d139b77156c353" }, + { url = "https://mirrors.aliyun.com/pypi/packages/b8/66/853d50e75a38c9a7370ddbeefabdd3d3116b9c31ef94dc92c6729bc36bec/psutil-7.1.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ad81425efc5e75da3f39b3e636293360ad8d0b49bed7df824c79764fb4ba9b8b" }, + { url = "https://mirrors.aliyun.com/pypi/packages/41/bd/313aba97cb5bfb26916dc29cf0646cbe4dd6a89ca69e8c6edce654876d39/psutil-7.1.3-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8f33a3702e167783a9213db10ad29650ebf383946e91bc77f28a5eb083496bc9" }, + { url = "https://mirrors.aliyun.com/pypi/packages/c2/fa/76e3c06e760927a0cfb5705eb38164254de34e9bd86db656d4dbaa228b04/psutil-7.1.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fac9cd332c67f4422504297889da5ab7e05fd11e3c4392140f7370f4208ded1f" }, + { url = "https://mirrors.aliyun.com/pypi/packages/0f/1d/5774a91607035ee5078b8fd747686ebec28a962f178712de100d00b78a32/psutil-7.1.3-cp314-cp314t-win_amd64.whl", hash = "sha256:3792983e23b69843aea49c8f5b8f115572c5ab64c153bada5270086a2123c7e7" }, + { url = "https://mirrors.aliyun.com/pypi/packages/00/ca/e426584bacb43a5cb1ac91fae1937f478cd8fbe5e4ff96574e698a2c77cd/psutil-7.1.3-cp314-cp314t-win_arm64.whl", hash = "sha256:31d77fcedb7529f27bb3a0472bea9334349f9a04160e8e6e5020f22c59893264" }, + { url = "https://mirrors.aliyun.com/pypi/packages/ef/94/46b9154a800253e7ecff5aaacdf8ebf43db99de4a2dfa18575b02548654e/psutil-7.1.3-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:2bdbcd0e58ca14996a42adf3621a6244f1bb2e2e528886959c72cf1e326677ab" }, + { url = "https://mirrors.aliyun.com/pypi/packages/68/3a/9f93cff5c025029a36d9a92fef47220ab4692ee7f2be0fba9f92813d0cb8/psutil-7.1.3-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:bc31fa00f1fbc3c3802141eede66f3a2d51d89716a194bf2cd6fc68310a19880" }, + { url = "https://mirrors.aliyun.com/pypi/packages/ce/b1/5f49af514f76431ba4eea935b8ad3725cdeb397e9245ab919dbc1d1dc20f/psutil-7.1.3-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3bb428f9f05c1225a558f53e30ccbad9930b11c3fc206836242de1091d3e7dd3" }, + { url = "https://mirrors.aliyun.com/pypi/packages/e0/95/992c8816a74016eb095e73585d747e0a8ea21a061ed3689474fabb29a395/psutil-7.1.3-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:56d974e02ca2c8eb4812c3f76c30e28836fffc311d55d979f1465c1feeb2b68b" }, + { url = "https://mirrors.aliyun.com/pypi/packages/55/4c/c3ed1a622b6ae2fd3c945a366e64eb35247a31e4db16cf5095e269e8eb3c/psutil-7.1.3-cp37-abi3-win_amd64.whl", hash = "sha256:f39c2c19fe824b47484b96f9692932248a54c43799a84282cfe58d05a6449efd" }, + { url = "https://mirrors.aliyun.com/pypi/packages/c9/ad/33b2ccec09bf96c2b2ef3f9a6f66baac8253d7565d8839e024a6b905d45d/psutil-7.1.3-cp37-abi3-win_arm64.whl", hash = "sha256:bd0d69cee829226a761e92f28140bec9a5ee9d5b4fb4b0cc589068dbfff559b1" }, ] [[package]] @@ -2025,7 +2035,7 @@ wheels = [ [[package]] name = "pydantic" -version = "2.12.3" +version = "2.12.4" source = { registry = "https://mirrors.aliyun.com/pypi/simple" } dependencies = [ { name = "annotated-types" }, @@ -2033,123 +2043,127 @@ dependencies = [ { name = "typing-extensions" }, { name = "typing-inspection" }, ] -sdist = { url = "https://mirrors.aliyun.com/pypi/packages/f3/1e/4f0a3233767010308f2fd6bd0814597e3f63f1dc98304a9112b8759df4ff/pydantic-2.12.3.tar.gz", hash = "sha256:1da1c82b0fc140bb0103bc1441ffe062154c8d38491189751ee00fd8ca65ce74" } +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/96/ad/a17bc283d7d81837c061c49e3eaa27a45991759a1b7eae1031921c6bd924/pydantic-2.12.4.tar.gz", hash = "sha256:0f8cb9555000a4b5b617f66bfd2566264c4984b27589d3b845685983e8ea85ac" } wheels = [ - { url = "https://mirrors.aliyun.com/pypi/packages/a1/6b/83661fa77dcefa195ad5f8cd9af3d1a7450fd57cc883ad04d65446ac2029/pydantic-2.12.3-py3-none-any.whl", hash = "sha256:6986454a854bc3bc6e5443e1369e06a3a456af9d339eda45510f517d9ea5c6bf" }, + { url = "https://mirrors.aliyun.com/pypi/packages/82/2f/e68750da9b04856e2a7ec56fc6f034a5a79775e9b9a81882252789873798/pydantic-2.12.4-py3-none-any.whl", hash = "sha256:92d3d202a745d46f9be6df459ac5a064fdaa3c1c4cd8adcfa332ccf3c05f871e" }, ] [[package]] name = "pydantic-core" -version = "2.41.4" +version = "2.41.5" source = { registry = "https://mirrors.aliyun.com/pypi/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://mirrors.aliyun.com/pypi/packages/df/18/d0944e8eaaa3efd0a91b0f1fc537d3be55ad35091b6a87638211ba691964/pydantic_core-2.41.4.tar.gz", hash = "sha256:70e47929a9d4a1905a67e4b687d5946026390568a8e952b92824118063cee4d5" } -wheels = [ - { url = "https://mirrors.aliyun.com/pypi/packages/a7/3d/9b8ca77b0f76fcdbf8bc6b72474e264283f461284ca84ac3fde570c6c49a/pydantic_core-2.41.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2442d9a4d38f3411f22eb9dd0912b7cbf4b7d5b6c92c4173b75d3e1ccd84e36e" }, - { url = "https://mirrors.aliyun.com/pypi/packages/59/92/b7b0fe6ed4781642232755cb7e56a86e2041e1292f16d9ae410a0ccee5ac/pydantic_core-2.41.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:30a9876226dda131a741afeab2702e2d127209bde3c65a2b8133f428bc5d006b" }, - { url = "https://mirrors.aliyun.com/pypi/packages/52/8c/3eb872009274ffa4fb6a9585114e161aa1a0915af2896e2d441642929fe4/pydantic_core-2.41.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d55bbac04711e2980645af68b97d445cdbcce70e5216de444a6c4b6943ebcccd" }, - { url = "https://mirrors.aliyun.com/pypi/packages/f4/21/35adf4a753bcfaea22d925214a0c5b880792e3244731b3f3e6fec0d124f7/pydantic_core-2.41.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e1d778fb7849a42d0ee5927ab0f7453bf9f85eef8887a546ec87db5ddb178945" }, - { url = "https://mirrors.aliyun.com/pypi/packages/7d/d0/cdf7d126825e36d6e3f1eccf257da8954452934ede275a8f390eac775e89/pydantic_core-2.41.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1b65077a4693a98b90ec5ad8f203ad65802a1b9b6d4a7e48066925a7e1606706" }, - { url = "https://mirrors.aliyun.com/pypi/packages/2e/1c/af1e6fd5ea596327308f9c8d1654e1285cc3d8de0d584a3c9d7705bf8a7c/pydantic_core-2.41.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:62637c769dee16eddb7686bf421be48dfc2fae93832c25e25bc7242e698361ba" }, - { url = "https://mirrors.aliyun.com/pypi/packages/d3/81/8cece29a6ef1b3a92f956ea6da6250d5b2d2e7e4d513dd3b4f0c7a83dfea/pydantic_core-2.41.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2dfe3aa529c8f501babf6e502936b9e8d4698502b2cfab41e17a028d91b1ac7b" }, - { url = "https://mirrors.aliyun.com/pypi/packages/e3/37/a6a579f5fc2cd4d5521284a0ab6a426cc6463a7b3897aeb95b12f1ba607b/pydantic_core-2.41.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ca2322da745bf2eeb581fc9ea3bbb31147702163ccbcbf12a3bb630e4bf05e1d" }, - { url = "https://mirrors.aliyun.com/pypi/packages/ae/03/505020dc5c54ec75ecba9f41119fd1e48f9e41e4629942494c4a8734ded1/pydantic_core-2.41.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e8cd3577c796be7231dcf80badcf2e0835a46665eaafd8ace124d886bab4d700" }, - { url = "https://mirrors.aliyun.com/pypi/packages/cb/5d/2c0d09fb53aa03bbd2a214d89ebfa6304be7df9ed86ee3dc7770257f41ee/pydantic_core-2.41.4-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:1cae8851e174c83633f0833e90636832857297900133705ee158cf79d40f03e6" }, - { url = "https://mirrors.aliyun.com/pypi/packages/ea/4b/c2c9c8f5e1f9c864b57d08539d9d3db160e00491c9f5ee90e1bfd905e644/pydantic_core-2.41.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a26d950449aae348afe1ac8be5525a00ae4235309b729ad4d3399623125b43c9" }, - { url = "https://mirrors.aliyun.com/pypi/packages/28/c3/a74c1c37f49c0a02c89c7340fafc0ba816b29bd495d1a31ce1bdeacc6085/pydantic_core-2.41.4-cp310-cp310-win32.whl", hash = "sha256:0cf2a1f599efe57fa0051312774280ee0f650e11152325e41dfd3018ef2c1b57" }, - { url = "https://mirrors.aliyun.com/pypi/packages/d6/23/5dd5c1324ba80303368f7569e2e2e1a721c7d9eb16acb7eb7b7f85cb1be2/pydantic_core-2.41.4-cp310-cp310-win_amd64.whl", hash = "sha256:a8c2e340d7e454dc3340d3d2e8f23558ebe78c98aa8f68851b04dcb7bc37abdc" }, - { url = "https://mirrors.aliyun.com/pypi/packages/62/4c/f6cbfa1e8efacd00b846764e8484fe173d25b8dab881e277a619177f3384/pydantic_core-2.41.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:28ff11666443a1a8cf2a044d6a545ebffa8382b5f7973f22c36109205e65dc80" }, - { url = "https://mirrors.aliyun.com/pypi/packages/21/f8/40b72d3868896bfcd410e1bd7e516e762d326201c48e5b4a06446f6cf9e8/pydantic_core-2.41.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:61760c3925d4633290292bad462e0f737b840508b4f722247d8729684f6539ae" }, - { url = "https://mirrors.aliyun.com/pypi/packages/94/4d/d203dce8bee7faeca791671c88519969d98d3b4e8f225da5b96dad226fc8/pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eae547b7315d055b0de2ec3965643b0ab82ad0106a7ffd29615ee9f266a02827" }, - { url = "https://mirrors.aliyun.com/pypi/packages/65/f5/6a66187775df87c24d526985b3a5d78d861580ca466fbd9d4d0e792fcf6c/pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ef9ee5471edd58d1fcce1c80ffc8783a650e3e3a193fe90d52e43bb4d87bff1f" }, - { url = "https://mirrors.aliyun.com/pypi/packages/5e/b9/78336345de97298cf53236b2f271912ce11f32c1e59de25a374ce12f9cce/pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:15dd504af121caaf2c95cb90c0ebf71603c53de98305621b94da0f967e572def" }, - { url = "https://mirrors.aliyun.com/pypi/packages/99/bb/a4584888b70ee594c3d374a71af5075a68654d6c780369df269118af7402/pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3a926768ea49a8af4d36abd6a8968b8790f7f76dd7cbd5a4c180db2b4ac9a3a2" }, - { url = "https://mirrors.aliyun.com/pypi/packages/5f/8d/17fc5de9d6418e4d2ae8c675f905cdafdc59d3bf3bf9c946b7ab796a992a/pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6916b9b7d134bff5440098a4deb80e4cb623e68974a87883299de9124126c2a8" }, - { url = "https://mirrors.aliyun.com/pypi/packages/54/e7/03d2c5c0b8ed37a4617430db68ec5e7dbba66358b629cd69e11b4d564367/pydantic_core-2.41.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5cf90535979089df02e6f17ffd076f07237efa55b7343d98760bde8743c4b265" }, - { url = "https://mirrors.aliyun.com/pypi/packages/be/fc/15d1c9fe5ad9266a5897d9b932b7f53d7e5cfc800573917a2c5d6eea56ec/pydantic_core-2.41.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:7533c76fa647fade2d7ec75ac5cc079ab3f34879626dae5689b27790a6cf5a5c" }, - { url = "https://mirrors.aliyun.com/pypi/packages/26/ef/e735dd008808226c83ba56972566138665b71477ad580fa5a21f0851df48/pydantic_core-2.41.4-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:37e516bca9264cbf29612539801ca3cd5d1be465f940417b002905e6ed79d38a" }, - { url = "https://mirrors.aliyun.com/pypi/packages/90/00/806efdcf35ff2ac0f938362350cd9827b8afb116cc814b6b75cf23738c7c/pydantic_core-2.41.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0c19cb355224037c83642429b8ce261ae108e1c5fbf5c028bac63c77b0f8646e" }, - { url = "https://mirrors.aliyun.com/pypi/packages/41/7e/6ac90673fe6cb36621a2283552897838c020db343fa86e513d3f563b196f/pydantic_core-2.41.4-cp311-cp311-win32.whl", hash = "sha256:09c2a60e55b357284b5f31f5ab275ba9f7f70b7525e18a132ec1f9160b4f1f03" }, - { url = "https://mirrors.aliyun.com/pypi/packages/e0/9d/7c5e24ee585c1f8b6356e1d11d40ab807ffde44d2db3b7dfd6d20b09720e/pydantic_core-2.41.4-cp311-cp311-win_amd64.whl", hash = "sha256:711156b6afb5cb1cb7c14a2cc2c4a8b4c717b69046f13c6b332d8a0a8f41ca3e" }, - { url = "https://mirrors.aliyun.com/pypi/packages/33/90/5c172357460fc28b2871eb4a0fb3843b136b429c6fa827e4b588877bf115/pydantic_core-2.41.4-cp311-cp311-win_arm64.whl", hash = "sha256:6cb9cf7e761f4f8a8589a45e49ed3c0d92d1d696a45a6feaee8c904b26efc2db" }, - { url = "https://mirrors.aliyun.com/pypi/packages/e9/81/d3b3e95929c4369d30b2a66a91db63c8ed0a98381ae55a45da2cd1cc1288/pydantic_core-2.41.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:ab06d77e053d660a6faaf04894446df7b0a7e7aba70c2797465a0a1af00fc887" }, - { url = "https://mirrors.aliyun.com/pypi/packages/58/da/46fdac49e6717e3a94fc9201403e08d9d61aa7a770fab6190b8740749047/pydantic_core-2.41.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c53ff33e603a9c1179a9364b0a24694f183717b2e0da2b5ad43c316c956901b2" }, - { url = "https://mirrors.aliyun.com/pypi/packages/1e/63/4d948f1b9dd8e991a5a98b77dd66c74641f5f2e5225fee37994b2e07d391/pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:304c54176af2c143bd181d82e77c15c41cbacea8872a2225dd37e6544dce9999" }, - { url = "https://mirrors.aliyun.com/pypi/packages/b2/a7/e5fc60a6f781fc634ecaa9ecc3c20171d238794cef69ae0af79ac11b89d7/pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:025ba34a4cf4fb32f917d5d188ab5e702223d3ba603be4d8aca2f82bede432a4" }, - { url = "https://mirrors.aliyun.com/pypi/packages/70/69/dce747b1d21d59e85af433428978a1893c6f8a7068fa2bb4a927fba7a5ff/pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b9f5f30c402ed58f90c70e12eff65547d3ab74685ffe8283c719e6bead8ef53f" }, - { url = "https://mirrors.aliyun.com/pypi/packages/83/6a/c070e30e295403bf29c4df1cb781317b6a9bac7cd07b8d3acc94d501a63c/pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd96e5d15385d301733113bcaa324c8bcf111275b7675a9c6e88bfb19fc05e3b" }, - { url = "https://mirrors.aliyun.com/pypi/packages/f0/83/06d001f8043c336baea7fd202a9ac7ad71f87e1c55d8112c50b745c40324/pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98f348cbb44fae6e9653c1055db7e29de67ea6a9ca03a5fa2c2e11a47cff0e47" }, - { url = "https://mirrors.aliyun.com/pypi/packages/14/0a/e567c2883588dd12bcbc110232d892cf385356f7c8a9910311ac997ab715/pydantic_core-2.41.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ec22626a2d14620a83ca583c6f5a4080fa3155282718b6055c2ea48d3ef35970" }, - { url = "https://mirrors.aliyun.com/pypi/packages/f4/1d/3d9fca34273ba03c9b1c5289f7618bc4bd09c3ad2289b5420481aa051a99/pydantic_core-2.41.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3a95d4590b1f1a43bf33ca6d647b990a88f4a3824a8c4572c708f0b45a5290ed" }, - { url = "https://mirrors.aliyun.com/pypi/packages/52/70/d702ef7a6cd41a8afc61f3554922b3ed8d19dd54c3bd4bdbfe332e610827/pydantic_core-2.41.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:f9672ab4d398e1b602feadcffcdd3af44d5f5e6ddc15bc7d15d376d47e8e19f8" }, - { url = "https://mirrors.aliyun.com/pypi/packages/68/4c/c06be6e27545d08b802127914156f38d10ca287a9e8489342793de8aae3c/pydantic_core-2.41.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:84d8854db5f55fead3b579f04bda9a36461dab0730c5d570e1526483e7bb8431" }, - { url = "https://mirrors.aliyun.com/pypi/packages/b0/e5/35ae4919bcd9f18603419e23c5eaf32750224a89d41a8df1a3704b69f77e/pydantic_core-2.41.4-cp312-cp312-win32.whl", hash = "sha256:9be1c01adb2ecc4e464392c36d17f97e9110fbbc906bcbe1c943b5b87a74aabd" }, - { url = "https://mirrors.aliyun.com/pypi/packages/1e/c2/49c5bb6d2a49eb2ee3647a93e3dae7080c6409a8a7558b075027644e879c/pydantic_core-2.41.4-cp312-cp312-win_amd64.whl", hash = "sha256:d682cf1d22bab22a5be08539dca3d1593488a99998f9f412137bc323179067ff" }, - { url = "https://mirrors.aliyun.com/pypi/packages/06/23/936343dbcba6eec93f73e95eb346810fc732f71ba27967b287b66f7b7097/pydantic_core-2.41.4-cp312-cp312-win_arm64.whl", hash = "sha256:833eebfd75a26d17470b58768c1834dfc90141b7afc6eb0429c21fc5a21dcfb8" }, - { url = "https://mirrors.aliyun.com/pypi/packages/13/d0/c20adabd181a029a970738dfe23710b52a31f1258f591874fcdec7359845/pydantic_core-2.41.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:85e050ad9e5f6fe1004eec65c914332e52f429bc0ae12d6fa2092407a462c746" }, - { url = "https://mirrors.aliyun.com/pypi/packages/00/b6/0ce5c03cec5ae94cca220dfecddc453c077d71363b98a4bbdb3c0b22c783/pydantic_core-2.41.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7393f1d64792763a48924ba31d1e44c2cfbc05e3b1c2c9abb4ceeadd912cced" }, - { url = "https://mirrors.aliyun.com/pypi/packages/68/3e/800d3d02c8beb0b5c069c870cbb83799d085debf43499c897bb4b4aaff0d/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94dab0940b0d1fb28bcab847adf887c66a27a40291eedf0b473be58761c9799a" }, - { url = "https://mirrors.aliyun.com/pypi/packages/60/a4/24271cc71a17f64589be49ab8bd0751f6a0a03046c690df60989f2f95c2c/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:de7c42f897e689ee6f9e93c4bec72b99ae3b32a2ade1c7e4798e690ff5246e02" }, - { url = "https://mirrors.aliyun.com/pypi/packages/68/de/45af3ca2f175d91b96bfb62e1f2d2f1f9f3b14a734afe0bfeff079f78181/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:664b3199193262277b8b3cd1e754fb07f2c6023289c815a1e1e8fb415cb247b1" }, - { url = "https://mirrors.aliyun.com/pypi/packages/af/8f/ae4e1ff84672bf869d0a77af24fd78387850e9497753c432875066b5d622/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d95b253b88f7d308b1c0b417c4624f44553ba4762816f94e6986819b9c273fb2" }, - { url = "https://mirrors.aliyun.com/pypi/packages/18/62/273dd70b0026a085c7b74b000394e1ef95719ea579c76ea2f0cc8893736d/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a1351f5bbdbbabc689727cb91649a00cb9ee7203e0a6e54e9f5ba9e22e384b84" }, - { url = "https://mirrors.aliyun.com/pypi/packages/30/03/cf485fff699b4cdaea469bc481719d3e49f023241b4abb656f8d422189fc/pydantic_core-2.41.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1affa4798520b148d7182da0615d648e752de4ab1a9566b7471bc803d88a062d" }, - { url = "https://mirrors.aliyun.com/pypi/packages/f9/7e/c8e713db32405dfd97211f2fc0a15d6bf8adb7640f3d18544c1f39526619/pydantic_core-2.41.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7b74e18052fea4aa8dea2fb7dbc23d15439695da6cbe6cfc1b694af1115df09d" }, - { url = "https://mirrors.aliyun.com/pypi/packages/04/f7/db71fd4cdccc8b75990f79ccafbbd66757e19f6d5ee724a6252414483fb4/pydantic_core-2.41.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:285b643d75c0e30abda9dc1077395624f314a37e3c09ca402d4015ef5979f1a2" }, - { url = "https://mirrors.aliyun.com/pypi/packages/76/63/a54973ddb945f1bca56742b48b144d85c9fc22f819ddeb9f861c249d5464/pydantic_core-2.41.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:f52679ff4218d713b3b33f88c89ccbf3a5c2c12ba665fb80ccc4192b4608dbab" }, - { url = "https://mirrors.aliyun.com/pypi/packages/f8/03/5d12891e93c19218af74843a27e32b94922195ded2386f7b55382f904d2f/pydantic_core-2.41.4-cp313-cp313-win32.whl", hash = "sha256:ecde6dedd6fff127c273c76821bb754d793be1024bc33314a120f83a3c69460c" }, - { url = "https://mirrors.aliyun.com/pypi/packages/be/d8/fd0de71f39db91135b7a26996160de71c073d8635edfce8b3c3681be0d6d/pydantic_core-2.41.4-cp313-cp313-win_amd64.whl", hash = "sha256:d081a1f3800f05409ed868ebb2d74ac39dd0c1ff6c035b5162356d76030736d4" }, - { url = "https://mirrors.aliyun.com/pypi/packages/72/86/c99921c1cf6650023c08bfab6fe2d7057a5142628ef7ccfa9921f2dda1d5/pydantic_core-2.41.4-cp313-cp313-win_arm64.whl", hash = "sha256:f8e49c9c364a7edcbe2a310f12733aad95b022495ef2a8d653f645e5d20c1564" }, - { url = "https://mirrors.aliyun.com/pypi/packages/36/0d/b5706cacb70a8414396efdda3d72ae0542e050b591119e458e2490baf035/pydantic_core-2.41.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:ed97fd56a561f5eb5706cebe94f1ad7c13b84d98312a05546f2ad036bafe87f4" }, - { url = "https://mirrors.aliyun.com/pypi/packages/de/2d/cba1fa02cfdea72dfb3a9babb067c83b9dff0bbcb198368e000a6b756ea7/pydantic_core-2.41.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a870c307bf1ee91fc58a9a61338ff780d01bfae45922624816878dce784095d2" }, - { url = "https://mirrors.aliyun.com/pypi/packages/07/ea/3df927c4384ed9b503c9cc2d076cf983b4f2adb0c754578dfb1245c51e46/pydantic_core-2.41.4-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d25e97bc1f5f8f7985bdc2335ef9e73843bb561eb1fa6831fdfc295c1c2061cf" }, - { url = "https://mirrors.aliyun.com/pypi/packages/6a/ee/df8e871f07074250270a3b1b82aad4cd0026b588acd5d7d3eb2fcb1471a3/pydantic_core-2.41.4-cp313-cp313t-win_amd64.whl", hash = "sha256:d405d14bea042f166512add3091c1af40437c2e7f86988f3915fabd27b1e9cd2" }, - { url = "https://mirrors.aliyun.com/pypi/packages/fc/de/b20f4ab954d6d399499c33ec4fafc46d9551e11dc1858fb7f5dca0748ceb/pydantic_core-2.41.4-cp313-cp313t-win_arm64.whl", hash = "sha256:19f3684868309db5263a11bace3c45d93f6f24afa2ffe75a647583df22a2ff89" }, - { url = "https://mirrors.aliyun.com/pypi/packages/54/28/d3325da57d413b9819365546eb9a6e8b7cbd9373d9380efd5f74326143e6/pydantic_core-2.41.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:e9205d97ed08a82ebb9a307e92914bb30e18cdf6f6b12ca4bedadb1588a0bfe1" }, - { url = "https://mirrors.aliyun.com/pypi/packages/9e/24/b58a1bc0d834bf1acc4361e61233ee217169a42efbdc15a60296e13ce438/pydantic_core-2.41.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:82df1f432b37d832709fbcc0e24394bba04a01b6ecf1ee87578145c19cde12ac" }, - { url = "https://mirrors.aliyun.com/pypi/packages/fb/a4/71f759cc41b7043e8ecdaab81b985a9b6cad7cec077e0b92cff8b71ecf6b/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc3b4cc4539e055cfa39a3763c939f9d409eb40e85813257dcd761985a108554" }, - { url = "https://mirrors.aliyun.com/pypi/packages/b0/64/1e79ac7aa51f1eec7c4cda8cbe456d5d09f05fdd68b32776d72168d54275/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b1eb1754fce47c63d2ff57fdb88c351a6c0150995890088b33767a10218eaa4e" }, - { url = "https://mirrors.aliyun.com/pypi/packages/e9/e3/a3ffc363bd4287b80f1d43dc1c28ba64831f8dfc237d6fec8f2661138d48/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e6ab5ab30ef325b443f379ddb575a34969c333004fca5a1daa0133a6ffaad616" }, - { url = "https://mirrors.aliyun.com/pypi/packages/28/27/78814089b4d2e684a9088ede3790763c64693c3d1408ddc0a248bc789126/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:31a41030b1d9ca497634092b46481b937ff9397a86f9f51bd41c4767b6fc04af" }, - { url = "https://mirrors.aliyun.com/pypi/packages/92/97/4de0e2a1159cb85ad737e03306717637842c88c7fd6d97973172fb183149/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a44ac1738591472c3d020f61c6df1e4015180d6262ebd39bf2aeb52571b60f12" }, - { url = "https://mirrors.aliyun.com/pypi/packages/0f/50/8cb90ce4b9efcf7ae78130afeb99fd1c86125ccdf9906ef64b9d42f37c25/pydantic_core-2.41.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d72f2b5e6e82ab8f94ea7d0d42f83c487dc159c5240d8f83beae684472864e2d" }, - { url = "https://mirrors.aliyun.com/pypi/packages/34/3b/ccdc77af9cd5082723574a1cc1bcae7a6acacc829d7c0a06201f7886a109/pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:c4d1e854aaf044487d31143f541f7aafe7b482ae72a022c664b2de2e466ed0ad" }, - { url = "https://mirrors.aliyun.com/pypi/packages/ca/ba/e7c7a02651a8f7c52dc2cff2b64a30c313e3b57c7d93703cecea76c09b71/pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b568af94267729d76e6ee5ececda4e283d07bbb28e8148bb17adad93d025d25a" }, - { url = "https://mirrors.aliyun.com/pypi/packages/2c/ba/6c533a4ee8aec6b812c643c49bb3bd88d3f01e3cebe451bb85512d37f00f/pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:6d55fb8b1e8929b341cc313a81a26e0d48aa3b519c1dbaadec3a6a2b4fcad025" }, - { url = "https://mirrors.aliyun.com/pypi/packages/22/ae/f10524fcc0ab8d7f96cf9a74c880243576fd3e72bd8ce4f81e43d22bcab7/pydantic_core-2.41.4-cp314-cp314-win32.whl", hash = "sha256:5b66584e549e2e32a1398df11da2e0a7eff45d5c2d9db9d5667c5e6ac764d77e" }, - { url = "https://mirrors.aliyun.com/pypi/packages/b4/dc/e5aa27aea1ad4638f0c3fb41132f7eb583bd7420ee63204e2d4333a3bbf9/pydantic_core-2.41.4-cp314-cp314-win_amd64.whl", hash = "sha256:557a0aab88664cc552285316809cab897716a372afaf8efdbef756f8b890e894" }, - { url = "https://mirrors.aliyun.com/pypi/packages/3e/61/51d89cc2612bd147198e120a13f150afbf0bcb4615cddb049ab10b81b79e/pydantic_core-2.41.4-cp314-cp314-win_arm64.whl", hash = "sha256:3f1ea6f48a045745d0d9f325989d8abd3f1eaf47dd00485912d1a3a63c623a8d" }, - { url = "https://mirrors.aliyun.com/pypi/packages/0d/c2/472f2e31b95eff099961fa050c376ab7156a81da194f9edb9f710f68787b/pydantic_core-2.41.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6c1fe4c5404c448b13188dd8bd2ebc2bdd7e6727fa61ff481bcc2cca894018da" }, - { url = "https://mirrors.aliyun.com/pypi/packages/4a/07/ea8eeb91173807ecdae4f4a5f4b150a520085b35454350fc219ba79e66a3/pydantic_core-2.41.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:523e7da4d43b113bf8e7b49fa4ec0c35bf4fe66b2230bfc5c13cc498f12c6c3e" }, - { url = "https://mirrors.aliyun.com/pypi/packages/1e/29/b53a9ca6cd366bfc928823679c6a76c7a4c69f8201c0ba7903ad18ebae2f/pydantic_core-2.41.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5729225de81fb65b70fdb1907fcf08c75d498f4a6f15af005aabb1fdadc19dfa" }, - { url = "https://mirrors.aliyun.com/pypi/packages/c7/3d/f8c1a371ceebcaf94d6dd2d77c6cf4b1c078e13a5837aee83f760b4f7cfd/pydantic_core-2.41.4-cp314-cp314t-win_amd64.whl", hash = "sha256:de2cfbb09e88f0f795fd90cf955858fc2c691df65b1f21f0aa00b99f3fbc661d" }, - { url = "https://mirrors.aliyun.com/pypi/packages/8a/ac/9fc61b4f9d079482a290afe8d206b8f490e9fd32d4fc03ed4fc698214e01/pydantic_core-2.41.4-cp314-cp314t-win_arm64.whl", hash = "sha256:d34f950ae05a83e0ede899c595f312ca976023ea1db100cd5aa188f7005e3ab0" }, - { url = "https://mirrors.aliyun.com/pypi/packages/b0/12/5ba58daa7f453454464f92b3ca7b9d7c657d8641c48e370c3ebc9a82dd78/pydantic_core-2.41.4-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:a1b2cfec3879afb742a7b0bcfa53e4f22ba96571c9e54d6a3afe1052d17d843b" }, - { url = "https://mirrors.aliyun.com/pypi/packages/21/fb/6860126a77725c3108baecd10fd3d75fec25191d6381b6eb2ac660228eac/pydantic_core-2.41.4-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:d175600d975b7c244af6eb9c9041f10059f20b8bbffec9e33fdd5ee3f67cdc42" }, - { url = "https://mirrors.aliyun.com/pypi/packages/de/be/57dcaa3ed595d81f8757e2b44a38240ac5d37628bce25fb20d02c7018776/pydantic_core-2.41.4-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0f184d657fa4947ae5ec9c47bd7e917730fa1cbb78195037e32dcbab50aca5ee" }, - { url = "https://mirrors.aliyun.com/pypi/packages/2f/1d/679a344fadb9695f1a6a294d739fbd21d71fa023286daeea8c0ed49e7c2b/pydantic_core-2.41.4-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ed810568aeffed3edc78910af32af911c835cc39ebbfacd1f0ab5dd53028e5c" }, - { url = "https://mirrors.aliyun.com/pypi/packages/c4/48/ae937e5a831b7c0dc646b2ef788c27cd003894882415300ed21927c21efa/pydantic_core-2.41.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:4f5d640aeebb438517150fdeec097739614421900e4a08db4a3ef38898798537" }, - { url = "https://mirrors.aliyun.com/pypi/packages/5e/db/6db8073e3d32dae017da7e0d16a9ecb897d0a4d92e00634916e486097961/pydantic_core-2.41.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:4a9ab037b71927babc6d9e7fc01aea9e66dc2a4a34dff06ef0724a4049629f94" }, - { url = "https://mirrors.aliyun.com/pypi/packages/0d/c1/dd3542d072fcc336030d66834872f0328727e3b8de289c662faa04aa270e/pydantic_core-2.41.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4dab9484ec605c3016df9ad4fd4f9a390bc5d816a3b10c6550f8424bb80b18c" }, - { url = "https://mirrors.aliyun.com/pypi/packages/2b/c6/db8d13a1f8ab3f1eb08c88bd00fd62d44311e3456d1e85c0e59e0a0376e7/pydantic_core-2.41.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8a5028425820731d8c6c098ab642d7b8b999758e24acae03ed38a66eca8335" }, - { url = "https://mirrors.aliyun.com/pypi/packages/5d/d4/912e976a2dd0b49f31c98a060ca90b353f3b73ee3ea2fd0030412f6ac5ec/pydantic_core-2.41.4-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:1e5ab4fc177dd41536b3c32b2ea11380dd3d4619a385860621478ac2d25ceb00" }, - { url = "https://mirrors.aliyun.com/pypi/packages/71/f0/66ec5a626c81eba326072d6ee2b127f8c139543f1bf609b4842978d37833/pydantic_core-2.41.4-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:3d88d0054d3fa11ce936184896bed3c1c5441d6fa483b498fac6a5d0dd6f64a9" }, - { url = "https://mirrors.aliyun.com/pypi/packages/c4/af/625626278ca801ea0a658c2dcf290dc9f21bb383098e99e7c6a029fccfc0/pydantic_core-2.41.4-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b2a054a8725f05b4b6503357e0ac1c4e8234ad3b0c2ac130d6ffc66f0e170e2" }, - { url = "https://mirrors.aliyun.com/pypi/packages/20/f6/2fba049f54e0f4975fef66be654c597a1d005320fa141863699180c7697d/pydantic_core-2.41.4-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b0d9db5a161c99375a0c68c058e227bee1d89303300802601d76a3d01f74e258" }, - { url = "https://mirrors.aliyun.com/pypi/packages/0e/80/65ab839a2dfcd3b949202f9d920c34f9de5a537c3646662bdf2f7d999680/pydantic_core-2.41.4-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:6273ea2c8ffdac7b7fda2653c49682db815aebf4a89243a6feccf5e36c18c347" }, - { url = "https://mirrors.aliyun.com/pypi/packages/44/58/627565d3d182ce6dfda18b8e1c841eede3629d59c9d7cbc1e12a03aeb328/pydantic_core-2.41.4-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:4c973add636efc61de22530b2ef83a65f39b6d6f656df97f678720e20de26caa" }, - { url = "https://mirrors.aliyun.com/pypi/packages/24/06/8a84711162ad5a5f19a88cead37cca81b4b1f294f46260ef7334ae4f24d3/pydantic_core-2.41.4-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:b69d1973354758007f46cf2d44a4f3d0933f10b6dc9bf15cf1356e037f6f731a" }, - { url = "https://mirrors.aliyun.com/pypi/packages/aa/8b/b7bb512a4682a2f7fbfae152a755d37351743900226d29bd953aaf870eaa/pydantic_core-2.41.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:3619320641fd212aaf5997b6ca505e97540b7e16418f4a241f44cdf108ffb50d" }, - { url = "https://mirrors.aliyun.com/pypi/packages/7e/7d/138e902ed6399b866f7cfe4435d22445e16fff888a1c00560d9dc79a780f/pydantic_core-2.41.4-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:491535d45cd7ad7e4a2af4a5169b0d07bebf1adfd164b0368da8aa41e19907a5" }, - { url = "https://mirrors.aliyun.com/pypi/packages/47/13/0525623cf94627f7b53b4c2034c81edc8491cbfc7c28d5447fa318791479/pydantic_core-2.41.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:54d86c0cada6aba4ec4c047d0e348cbad7063b87ae0f005d9f8c9ad04d4a92a2" }, - { url = "https://mirrors.aliyun.com/pypi/packages/d6/f9/744bc98137d6ef0a233f808bfc9b18cf94624bf30836a18d3b05d08bf418/pydantic_core-2.41.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eca1124aced216b2500dc2609eade086d718e8249cb9696660ab447d50a758bd" }, - { url = "https://mirrors.aliyun.com/pypi/packages/17/c8/629e88920171173f6049386cc71f893dff03209a9ef32b4d2f7e7c264bcf/pydantic_core-2.41.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6c9024169becccf0cb470ada03ee578d7348c119a0d42af3dcf9eda96e3a247c" }, - { url = "https://mirrors.aliyun.com/pypi/packages/2e/0f/4f2734688d98488782218ca61bcc118329bf5de05bb7fe3adc7dd79b0b86/pydantic_core-2.41.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:26895a4268ae5a2849269f4991cdc97236e4b9c010e51137becf25182daac405" }, - { url = "https://mirrors.aliyun.com/pypi/packages/ed/f2/ab385dbd94a052c62224b99cf99002eee99dbec40e10006c78575aead256/pydantic_core-2.41.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:ca4df25762cf71308c446e33c9b1fdca2923a3f13de616e2a949f38bf21ff5a8" }, - { url = "https://mirrors.aliyun.com/pypi/packages/fc/8e/e4f12afe1beeb9823bba5375f8f258df0cc61b056b0195fb1cf9f62a1a58/pydantic_core-2.41.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:5a28fcedd762349519276c36634e71853b4541079cab4acaaac60c4421827308" }, - { url = "https://mirrors.aliyun.com/pypi/packages/48/f7/925f65d930802e3ea2eb4d5afa4cb8730c8dc0d2cb89a59dc4ed2fcb2d74/pydantic_core-2.41.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c173ddcd86afd2535e2b695217e82191580663a1d1928239f877f5a1649ef39f" }, +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e" } +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/c6/90/32c9941e728d564b411d574d8ee0cf09b12ec978cb22b294995bae5549a5/pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146" }, + { url = "https://mirrors.aliyun.com/pypi/packages/fb/a8/61c96a77fe28993d9a6fb0f4127e05430a267b235a124545d79fea46dd65/pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2" }, + { url = "https://mirrors.aliyun.com/pypi/packages/5d/b6/338abf60225acc18cdc08b4faef592d0310923d19a87fba1faf05af5346e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97" }, + { url = "https://mirrors.aliyun.com/pypi/packages/d1/1c/2ed0433e682983d8e8cba9c8d8ef274d4791ec6a6f24c58935b90e780e0a/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9" }, + { url = "https://mirrors.aliyun.com/pypi/packages/b3/24/cf84974ee7d6eae06b9e63289b7b8f6549d416b5c199ca2d7ce13bbcf619/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52" }, + { url = "https://mirrors.aliyun.com/pypi/packages/fd/21/4e287865504b3edc0136c89c9c09431be326168b1eb7841911cbc877a995/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941" }, + { url = "https://mirrors.aliyun.com/pypi/packages/a8/76/7727ef2ffa4b62fcab916686a68a0426b9b790139720e1934e8ba797e238/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a" }, + { url = "https://mirrors.aliyun.com/pypi/packages/d5/8c/a4abfc79604bcb4c748e18975c44f94f756f08fb04218d5cb87eb0d3a63e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c" }, + { url = "https://mirrors.aliyun.com/pypi/packages/67/b1/de2e9a9a79b480f9cb0b6e8b6ba4c50b18d4e89852426364c66aa82bb7b3/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2" }, + { url = "https://mirrors.aliyun.com/pypi/packages/16/c1/dfb33f837a47b20417500efaa0378adc6635b3c79e8369ff7a03c494b4ac/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556" }, + { url = "https://mirrors.aliyun.com/pypi/packages/47/36/00f398642a0f4b815a9a558c4f1dca1b4020a7d49562807d7bc9ff279a6c/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49" }, + { url = "https://mirrors.aliyun.com/pypi/packages/7e/70/cad3acd89fde2010807354d978725ae111ddf6d0ea46d1ea1775b5c1bd0c/pydantic_core-2.41.5-cp310-cp310-win32.whl", hash = "sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba" }, + { url = "https://mirrors.aliyun.com/pypi/packages/76/92/d338652464c6c367e5608e4488201702cd1cbb0f33f7b6a85a60fe5f3720/pydantic_core-2.41.5-cp310-cp310-win_amd64.whl", hash = "sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9" }, + { url = "https://mirrors.aliyun.com/pypi/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6" }, + { url = "https://mirrors.aliyun.com/pypi/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b" }, + { url = "https://mirrors.aliyun.com/pypi/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a" }, + { url = "https://mirrors.aliyun.com/pypi/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8" }, + { url = "https://mirrors.aliyun.com/pypi/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e" }, + { url = "https://mirrors.aliyun.com/pypi/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1" }, + { url = "https://mirrors.aliyun.com/pypi/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b" }, + { url = "https://mirrors.aliyun.com/pypi/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b" }, + { url = "https://mirrors.aliyun.com/pypi/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284" }, + { url = "https://mirrors.aliyun.com/pypi/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594" }, + { url = "https://mirrors.aliyun.com/pypi/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e" }, + { url = "https://mirrors.aliyun.com/pypi/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b" }, + { url = "https://mirrors.aliyun.com/pypi/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe" }, + { url = "https://mirrors.aliyun.com/pypi/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f" }, + { url = "https://mirrors.aliyun.com/pypi/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7" }, + { url = "https://mirrors.aliyun.com/pypi/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0" }, + { url = "https://mirrors.aliyun.com/pypi/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69" }, + { url = "https://mirrors.aliyun.com/pypi/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75" }, + { url = "https://mirrors.aliyun.com/pypi/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05" }, + { url = "https://mirrors.aliyun.com/pypi/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc" }, + { url = "https://mirrors.aliyun.com/pypi/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c" }, + { url = "https://mirrors.aliyun.com/pypi/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5" }, + { url = "https://mirrors.aliyun.com/pypi/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c" }, + { url = "https://mirrors.aliyun.com/pypi/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294" }, + { url = "https://mirrors.aliyun.com/pypi/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1" }, + { url = "https://mirrors.aliyun.com/pypi/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d" }, + { url = "https://mirrors.aliyun.com/pypi/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815" }, + { url = "https://mirrors.aliyun.com/pypi/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3" }, + { url = "https://mirrors.aliyun.com/pypi/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9" }, + { url = "https://mirrors.aliyun.com/pypi/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34" }, + { url = "https://mirrors.aliyun.com/pypi/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0" }, + { url = "https://mirrors.aliyun.com/pypi/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33" }, + { url = "https://mirrors.aliyun.com/pypi/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e" }, + { url = "https://mirrors.aliyun.com/pypi/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2" }, + { url = "https://mirrors.aliyun.com/pypi/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586" }, + { url = "https://mirrors.aliyun.com/pypi/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d" }, + { url = "https://mirrors.aliyun.com/pypi/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740" }, + { url = "https://mirrors.aliyun.com/pypi/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e" }, + { url = "https://mirrors.aliyun.com/pypi/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858" }, + { url = "https://mirrors.aliyun.com/pypi/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36" }, + { url = "https://mirrors.aliyun.com/pypi/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11" }, + { url = "https://mirrors.aliyun.com/pypi/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd" }, + { url = "https://mirrors.aliyun.com/pypi/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a" }, + { url = "https://mirrors.aliyun.com/pypi/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14" }, + { url = "https://mirrors.aliyun.com/pypi/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1" }, + { url = "https://mirrors.aliyun.com/pypi/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66" }, + { url = "https://mirrors.aliyun.com/pypi/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869" }, + { url = "https://mirrors.aliyun.com/pypi/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2" }, + { url = "https://mirrors.aliyun.com/pypi/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375" }, + { url = "https://mirrors.aliyun.com/pypi/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553" }, + { url = "https://mirrors.aliyun.com/pypi/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90" }, + { url = "https://mirrors.aliyun.com/pypi/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07" }, + { url = "https://mirrors.aliyun.com/pypi/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb" }, + { url = "https://mirrors.aliyun.com/pypi/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23" }, + { url = "https://mirrors.aliyun.com/pypi/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf" }, + { url = "https://mirrors.aliyun.com/pypi/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0" }, + { url = "https://mirrors.aliyun.com/pypi/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a" }, + { url = "https://mirrors.aliyun.com/pypi/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3" }, + { url = "https://mirrors.aliyun.com/pypi/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c" }, + { url = "https://mirrors.aliyun.com/pypi/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612" }, + { url = "https://mirrors.aliyun.com/pypi/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d" }, + { url = "https://mirrors.aliyun.com/pypi/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9" }, + { url = "https://mirrors.aliyun.com/pypi/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660" }, + { url = "https://mirrors.aliyun.com/pypi/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9" }, + { url = "https://mirrors.aliyun.com/pypi/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3" }, + { url = "https://mirrors.aliyun.com/pypi/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf" }, + { url = "https://mirrors.aliyun.com/pypi/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470" }, + { url = "https://mirrors.aliyun.com/pypi/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa" }, + { url = "https://mirrors.aliyun.com/pypi/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c" }, + { url = "https://mirrors.aliyun.com/pypi/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008" }, + { url = "https://mirrors.aliyun.com/pypi/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034" }, + { url = "https://mirrors.aliyun.com/pypi/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c" }, + { url = "https://mirrors.aliyun.com/pypi/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2" }, + { url = "https://mirrors.aliyun.com/pypi/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad" }, + { url = "https://mirrors.aliyun.com/pypi/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd" }, + { url = "https://mirrors.aliyun.com/pypi/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc" }, + { url = "https://mirrors.aliyun.com/pypi/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56" }, + { url = "https://mirrors.aliyun.com/pypi/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b" }, + { url = "https://mirrors.aliyun.com/pypi/packages/e6/b0/1a2aa41e3b5a4ba11420aba2d091b2d17959c8d1519ece3627c371951e73/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8" }, + { url = "https://mirrors.aliyun.com/pypi/packages/a4/ee/31b1f0020baaf6d091c87900ae05c6aeae101fa4e188e1613c80e4f1ea31/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a" }, + { url = "https://mirrors.aliyun.com/pypi/packages/e1/89/ab8e86208467e467a80deaca4e434adac37b10a9d134cd2f99b28a01e483/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b" }, + { url = "https://mirrors.aliyun.com/pypi/packages/99/0a/99a53d06dd0348b2008f2f30884b34719c323f16c3be4e6cc1203b74a91d/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2" }, + { url = "https://mirrors.aliyun.com/pypi/packages/6d/94/30ca3b73c6d485b9bb0bc66e611cff4a7138ff9736b7e66bcf0852151636/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093" }, + { url = "https://mirrors.aliyun.com/pypi/packages/87/57/31b4f8e12680b739a91f472b5671294236b82586889ef764b5fbc6669238/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a" }, + { url = "https://mirrors.aliyun.com/pypi/packages/7d/73/3c2c8edef77b8f7310e6fb012dbc4b8551386ed575b9eb6fb2506e28a7eb/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963" }, + { url = "https://mirrors.aliyun.com/pypi/packages/2f/02/8559b1f26ee0d502c74f9cca5c0d2fd97e967e083e006bbbb4e97f3a043a/pydantic_core-2.41.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a" }, + { url = "https://mirrors.aliyun.com/pypi/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26" }, + { url = "https://mirrors.aliyun.com/pypi/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808" }, + { url = "https://mirrors.aliyun.com/pypi/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc" }, + { url = "https://mirrors.aliyun.com/pypi/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1" }, + { url = "https://mirrors.aliyun.com/pypi/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84" }, + { url = "https://mirrors.aliyun.com/pypi/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770" }, + { url = "https://mirrors.aliyun.com/pypi/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f" }, + { url = "https://mirrors.aliyun.com/pypi/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51" }, ] [[package]] @@ -2607,15 +2621,15 @@ wheels = [ [[package]] name = "starlette" -version = "0.49.1" +version = "0.49.3" source = { registry = "https://mirrors.aliyun.com/pypi/simple" } dependencies = [ { name = "anyio" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://mirrors.aliyun.com/pypi/packages/1b/3f/507c21db33b66fb027a332f2cb3abbbe924cc3a79ced12f01ed8645955c9/starlette-0.49.1.tar.gz", hash = "sha256:481a43b71e24ed8c43b11ea02f5353d77840e01480881b8cb5a26b8cae64a8cb" } +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/de/1a/608df0b10b53b0beb96a37854ee05864d182ddd4b1156a22f1ad3860425a/starlette-0.49.3.tar.gz", hash = "sha256:1c14546f299b5901a1ea0e34410575bc33bbd741377a10484a54445588d00284" } wheels = [ - { url = "https://mirrors.aliyun.com/pypi/packages/51/da/545b75d420bb23b5d494b0517757b351963e974e79933f01e05c929f20a6/starlette-0.49.1-py3-none-any.whl", hash = "sha256:d92ce9f07e4a3caa3ac13a79523bd18e3bc0042bb8ff2d759a8e7dd0e1859875" }, + { url = "https://mirrors.aliyun.com/pypi/packages/a3/e0/021c772d6a662f43b63044ab481dc6ac7592447605b5b35a957785363122/starlette-0.49.3-py3-none-any.whl", hash = "sha256:b579b99715fdc2980cf88c8ec96d3bf1ce16f5a8051a7c2b84ef9b1cdecaea2f" }, ] [[package]] From 1885098a6ce10642f1f5add61c5603a84d6be5c2 Mon Sep 17 00:00:00 2001 From: Wu Clan Date: Thu, 6 Nov 2025 21:40:40 +0800 Subject: [PATCH 02/25] Revert of some changes --- backend/app/admin/crud/crud_user.py | 41 ++++++++++++----------------- 1 file changed, 17 insertions(+), 24 deletions(-) diff --git a/backend/app/admin/crud/crud_user.py b/backend/app/admin/crud/crud_user.py index 92df7c9f..f140f272 100644 --- a/backend/app/admin/crud/crud_user.py +++ b/backend/app/admin/crud/crud_user.py @@ -2,8 +2,9 @@ import bcrypt -from sqlalchemy import delete, insert, select +from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.orm import noload, selectinload from sqlalchemy.sql import Select from sqlalchemy_crud_plus import CRUDPlus, JoinConfig @@ -76,13 +77,9 @@ async def add(self, db: AsyncSession, obj: AddUserParam) -> None: dict_obj.update({'salt': salt}) new_user = self.model(**dict_obj) - db.add(new_user) - await db.flush() # 获取用户ID - - # 添加用户角色关联(逻辑外键) - if obj.roles: - for role_id in obj.roles: - await db.execute(insert(user_role).values(user_id=new_user.id, role_id=role_id)) + stmt = select(Role).where(Role.id.in_(obj.roles)) + roles = await db.execute(stmt) + new_user.roles = roles.scalars().all() async def add_by_oauth2(self, db: AsyncSession, obj: AddOAuth2UserParam) -> None: """ @@ -96,15 +93,11 @@ async def add_by_oauth2(self, db: AsyncSession, obj: AddOAuth2UserParam) -> None dict_obj.update({'is_staff': True, 'salt': None}) new_user = self.model(**dict_obj) - db.add(new_user) - await db.flush() # 获取用户ID - - # 绑定第一个角色(逻辑外键) - stmt = select(Role).limit(1) + stmt = select(Role) role = await db.execute(stmt) - first_role = role.scalars().first() - if first_role: - await db.execute(insert(user_role).values(user_id=new_user.id, role_id=first_role.id)) + new_user.roles = [role.scalars().first()] # 默认绑定第一个角色 + + db.add(new_user) async def update(self, db: AsyncSession, input_user: User, obj: UpdateUserParam) -> int: """ @@ -120,13 +113,9 @@ async def update(self, db: AsyncSession, input_user: User, obj: UpdateUserParam) count = await self.update_model(db, input_user.id, obj) - # 删除原有用户角色关联 - await db.execute(delete(user_role).where(user_role.c.user_id == input_user.id)) - - # 添加新的用户角色关联(逻辑外键) - if role_ids: - for role_id in role_ids: - await db.execute(insert(user_role).values(user_id=input_user.id, role_id=role_id)) + stmt = select(Role).where(Role.id.in_(role_ids)) + roles = await db.execute(stmt) + input_user.roles = roles.scalars().all() return count @@ -220,6 +209,10 @@ async def get_select(self, dept: int | None, username: str | None, phone: str | return await self.select_order( 'id', 'desc', + load_options=[ + selectinload(self.model.dept).options(noload(Dept.parent), noload(Dept.children), noload(Dept.users)), + selectinload(self.model.roles).options(noload(Role.users), noload(Role.menus), noload(Role.scopes)), + ], **filters, ) @@ -280,7 +273,7 @@ async def get_joins( :param db: 数据库会话 :param user_id: 用户 ID :param username: 用户名 - :return: 包含用户信息及关联部门、角色等数据的对象,支持访问 .status、.dept、.roles 等属性 + :return: """ filters = {} From c50fdadf5cd9118772f184ebaa797b22c52433ac Mon Sep 17 00:00:00 2001 From: Wu Clan Date: Thu, 6 Nov 2025 21:41:54 +0800 Subject: [PATCH 03/25] More revert --- backend/app/admin/crud/crud_user.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/app/admin/crud/crud_user.py b/backend/app/admin/crud/crud_user.py index f140f272..352e3a81 100644 --- a/backend/app/admin/crud/crud_user.py +++ b/backend/app/admin/crud/crud_user.py @@ -81,6 +81,8 @@ async def add(self, db: AsyncSession, obj: AddUserParam) -> None: roles = await db.execute(stmt) new_user.roles = roles.scalars().all() + db.add(new_user) + async def add_by_oauth2(self, db: AsyncSession, obj: AddOAuth2UserParam) -> None: """ 通过 OAuth2 添加用户 @@ -116,7 +118,6 @@ async def update(self, db: AsyncSession, input_user: User, obj: UpdateUserParam) stmt = select(Role).where(Role.id.in_(role_ids)) roles = await db.execute(stmt) input_user.roles = roles.scalars().all() - return count async def update_nickname(self, db: AsyncSession, user_id: int, nickname: str) -> int: From 7af81d1e17f39921e9c9cf6cbb62dca6f419db3c Mon Sep 17 00:00:00 2001 From: Wu Clan Date: Fri, 7 Nov 2025 15:55:44 +0800 Subject: [PATCH 04/25] Update the user paginate --- backend/app/admin/crud/crud_user.py | 23 ++++++++++++++-------- backend/app/admin/service/user_service.py | 4 +--- backend/middleware/opera_log_middleware.py | 5 +---- backend/utils/serializers.py | 23 +++++++++++----------- 4 files changed, 29 insertions(+), 26 deletions(-) diff --git a/backend/app/admin/crud/crud_user.py b/backend/app/admin/crud/crud_user.py index 352e3a81..459c7e09 100644 --- a/backend/app/admin/crud/crud_user.py +++ b/backend/app/admin/crud/crud_user.py @@ -4,8 +4,6 @@ from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession -from sqlalchemy.orm import noload, selectinload -from sqlalchemy.sql import Select from sqlalchemy_crud_plus import CRUDPlus, JoinConfig from backend.app.admin.model import DataRule, DataScope, Dept, Menu, Role, User @@ -15,6 +13,7 @@ AddUserParam, UpdateUserParam, ) +from backend.common.pagination import paging_data from backend.common.security.jwt import get_hash_password from backend.utils.serializers import select_join_serialize from backend.utils.timezone import timezone @@ -186,10 +185,13 @@ async def reset_password(self, db: AsyncSession, pk: int, password: str) -> int: new_pwd = get_hash_password(password, salt) return await self.update_model(db, pk, {'password': new_pwd, 'salt': salt}) - async def get_select(self, dept: int | None, username: str | None, phone: str | None, status: int | None) -> Select: + async def get_paginated( + self, db: AsyncSession, dept: int | None, username: str | None, phone: str | None, status: int | None + ) -> dict[str, Any]: """ - 获取用户列表查询表达式 + 获取用户分页 + :param db: 数据库会话 :param dept: 部门 ID :param username: 用户名 :param phone: 电话号码 @@ -207,16 +209,21 @@ async def get_select(self, dept: int | None, username: str | None, phone: str | if status is not None: filters['status'] = status - return await self.select_order( + user_select = await self.select_order( 'id', 'desc', - load_options=[ - selectinload(self.model.dept).options(noload(Dept.parent), noload(Dept.children), noload(Dept.users)), - selectinload(self.model.roles).options(noload(Role.users), noload(Role.menus), noload(Role.scopes)), + join_conditions=[ + JoinConfig(model=Dept, join_on=Dept.id == self.model.dept_id, fill_result=True), + JoinConfig(model=user_role, join_on=user_role.c.user_id == self.model.id), + JoinConfig(model=Role, join_on=Role.id == user_role.c.role_id, fill_result=True), ], **filters, ) + data = await paging_data(db, user_select) + data['items'] = select_join_serialize(data['items'], relationships=['User-m2o-Dept', 'User-m2m-Role']) + return data + async def set_super(self, db: AsyncSession, user_id: int, *, is_super: bool) -> int: """ 设置用户超级管理员状态 diff --git a/backend/app/admin/service/user_service.py b/backend/app/admin/service/user_service.py index c5eb1eb4..56c0d705 100644 --- a/backend/app/admin/service/user_service.py +++ b/backend/app/admin/service/user_service.py @@ -18,7 +18,6 @@ from backend.common.context import ctx from backend.common.enums import UserPermissionType from backend.common.exception import errors -from backend.common.pagination import paging_data from backend.common.response.response_code import CustomErrorCode from backend.common.security.jwt import get_token, jwt_decode, password_verify from backend.core.conf import settings @@ -69,8 +68,7 @@ async def get_list(*, db: AsyncSession, dept: int, username: str, phone: str, st :param status: 状态 :return: """ - user_select = await user_dao.get_select(dept=dept, username=username, phone=phone, status=status) - return await paging_data(db, user_select) + return await user_dao.get_paginated(db=db, dept=dept, username=username, phone=phone, status=status) @staticmethod async def create(*, db: AsyncSession, obj: AddUserParam) -> None: diff --git a/backend/middleware/opera_log_middleware.py b/backend/middleware/opera_log_middleware.py index 3895e64c..a4cfa91c 100644 --- a/backend/middleware/opera_log_middleware.py +++ b/backend/middleware/opera_log_middleware.py @@ -86,10 +86,7 @@ async def dispatch(self, request: Request, call_next: Any) -> Response: log.debug(f'接口摘要:[{summary}]') log.debug(f'请求地址:[{ctx.ip}]') log.debug(f'请求参数:{args}') - log.info( - f'{request.client.host: <15} | {request.method: <8} | {response.status_code: <6} | ' - f'{path} | {elapsed:.3f}ms', - ) + log.info(f'{request.client.host: <15} | {request.method: <8} | {code!s: <6} | {path} | {elapsed:.3f}ms') if request.method != 'OPTIONS': log.debug('<-- 请求结束') diff --git a/backend/utils/serializers.py b/backend/utils/serializers.py index ec85e364..0d3fd041 100644 --- a/backend/utils/serializers.py +++ b/backend/utils/serializers.py @@ -18,6 +18,15 @@ R = TypeVar('R', bound=RowData) +class MsgSpecJSONResponse(JSONResponse): + """ + 使用高性能的 msgspec 库将数据序列化为 JSON 的响应类 + """ + + def render(self, content: Any) -> bytes: + return json.encode(content) + + def select_columns_serialize(row: R) -> dict[str, Any]: """ 序列化 SQLAlchemy 查询表的列,不包含关联列 @@ -79,7 +88,8 @@ def select_as_dict(row: R, *, use_alias: bool = False) -> dict[str, Any]: def select_join_serialize( # noqa: C901 row: R | Sequence[R], relationships: list[str] | None = None, - return_as_dict: bool = False, # noqa: FBT001, FBT002 + *, + return_as_dict: bool = False, ) -> dict[str, Any] | list[dict[str, Any]] | tuple[Any, ...] | list[tuple[Any, ...]] | None: """ 将 SQLAlchemy 连接查询结果序列化为支持属性访问的 namedtuple,支持虚拟关系嵌套 @@ -92,7 +102,7 @@ def select_join_serialize( # noqa: C901 - 输出:Result(name='Alice', dept=Dept(...)) :param row: SQLAlchemy 查询结果 - :param relationships: 虚拟关系 (source_model_class-type-target_model_class, type: o2m/m2o/o2o/m2m) + :param relationships: 表之间的虚拟关系 (source_model_class-type-target_model_class, type: o2m/m2o/o2o/m2m) :param return_as_dict: False 返回 namedtuple,True 返回 dict :return: """ @@ -329,12 +339,3 @@ def build_nested(layer_name: str, parent_id_: int, main_id_: int) -> list: result_list.append(nest_data) return result_list[0] if len(result_list) == 1 else result_list - - -class MsgSpecJSONResponse(JSONResponse): - """ - 使用高性能的 msgspec 库将数据序列化为 JSON 的响应类 - """ - - def render(self, content: Any) -> bytes: - return json.encode(content) From 7cf1aea694fe221d9fd5e5a02c08b21996c2c99a Mon Sep 17 00:00:00 2001 From: Wu Clan Date: Fri, 7 Nov 2025 18:30:27 +0800 Subject: [PATCH 05/25] Update user create and update --- backend/app/admin/crud/crud_user.py | 38 ++++++++++++++++++++--------- backend/app/admin/schema/user.py | 7 ++++++ 2 files changed, 33 insertions(+), 12 deletions(-) diff --git a/backend/app/admin/crud/crud_user.py b/backend/app/admin/crud/crud_user.py index 459c7e09..03e601cb 100644 --- a/backend/app/admin/crud/crud_user.py +++ b/backend/app/admin/crud/crud_user.py @@ -2,7 +2,7 @@ import bcrypt -from sqlalchemy import select +from sqlalchemy import insert, select from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy_crud_plus import CRUDPlus, JoinConfig @@ -11,6 +11,7 @@ from backend.app.admin.schema.user import ( AddOAuth2UserParam, AddUserParam, + AddUserRoleParam, UpdateUserParam, ) from backend.common.pagination import paging_data @@ -72,15 +73,20 @@ async def add(self, db: AsyncSession, obj: AddUserParam) -> None: """ salt = bcrypt.gensalt() obj.password = get_hash_password(obj.password, salt) + dict_obj = obj.model_dump(exclude={'roles'}) dict_obj.update({'salt': salt}) new_user = self.model(**dict_obj) + db.add(new_user) + await db.flush() - stmt = select(Role).where(Role.id.in_(obj.roles)) - roles = await db.execute(stmt) - new_user.roles = roles.scalars().all() + role_stmt = select(Role).where(Role.id.in_(obj.roles)) + result = await db.execute(role_stmt) + roles = result.scalars().all() - db.add(new_user) + user_role_data = [AddUserRoleParam(user_id=new_user.id, role_id=role.id).model_dump() for role in roles] + user_role_stmt = insert(user_role) + await db.execute(user_role_stmt, user_role_data) async def add_by_oauth2(self, db: AsyncSession, obj: AddOAuth2UserParam) -> None: """ @@ -93,12 +99,15 @@ async def add_by_oauth2(self, db: AsyncSession, obj: AddOAuth2UserParam) -> None dict_obj = obj.model_dump() dict_obj.update({'is_staff': True, 'salt': None}) new_user = self.model(**dict_obj) + db.add(new_user) + await db.flush() - stmt = select(Role) - role = await db.execute(stmt) - new_user.roles = [role.scalars().first()] # 默认绑定第一个角色 + role_stmt = select(Role) + result = await db.execute(role_stmt) + role = result.scalars().first() # 默认绑定第一个角色 - db.add(new_user) + user_role_stmt = insert(user_role).values(AddUserRoleParam(user_id=new_user.id, role_id=role.id).model_dump()) + await db.execute(user_role_stmt) async def update(self, db: AsyncSession, input_user: User, obj: UpdateUserParam) -> int: """ @@ -114,9 +123,14 @@ async def update(self, db: AsyncSession, input_user: User, obj: UpdateUserParam) count = await self.update_model(db, input_user.id, obj) - stmt = select(Role).where(Role.id.in_(role_ids)) - roles = await db.execute(stmt) - input_user.roles = roles.scalars().all() + role_stmt = select(Role).where(Role.id.in_(role_ids)) + result = await db.execute(role_stmt) + roles = result.scalars().all() + + user_role_data = [AddUserRoleParam(user_id=input_user.id, role_id=role.id).model_dump() for role in roles] + user_role_stmt = insert(user_role) + await db.execute(user_role_stmt, user_role_data) + return count async def update_nickname(self, db: AsyncSession, user_id: int, nickname: str) -> int: diff --git a/backend/app/admin/schema/user.py b/backend/app/admin/schema/user.py index c17a8753..f650ef60 100644 --- a/backend/app/admin/schema/user.py +++ b/backend/app/admin/schema/user.py @@ -34,6 +34,13 @@ class AddUserParam(AuthSchemaBase): roles: list[int] = Field(description='角色 ID 列表') +class AddUserRoleParam(SchemaBase): + """添加用户角色""" + + user_id: int = Field(description='用户 ID') + role_id: int = Field(description='角色 ID') + + class AddOAuth2UserParam(AuthSchemaBase): """添加 OAuth2 用户参数""" From b19b23772845c08a3edb1b6ff877957ddacc3795 Mon Sep 17 00:00:00 2001 From: Wu Clan Date: Fri, 7 Nov 2025 23:46:32 +0800 Subject: [PATCH 06/25] Update dept select and delete --- .gitignore | 1 + backend/app/admin/crud/crud_dept.py | 21 ++++++++++++++------- backend/app/admin/service/dept_service.py | 4 +++- 3 files changed, 18 insertions(+), 8 deletions(-) diff --git a/.gitignore b/.gitignore index 779ee46e..da982ab7 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ venv/ .python-version .ruff_cache/ .pytest_cache/ +.claude/ diff --git a/backend/app/admin/crud/crud_dept.py b/backend/app/admin/crud/crud_dept.py index 54caded7..1fc7f124 100644 --- a/backend/app/admin/crud/crud_dept.py +++ b/backend/app/admin/crud/crud_dept.py @@ -1,12 +1,14 @@ from collections.abc import Sequence +from typing import Any from fastapi import Request from sqlalchemy.ext.asyncio import AsyncSession -from sqlalchemy_crud_plus import CRUDPlus +from sqlalchemy_crud_plus import CRUDPlus, JoinConfig -from backend.app.admin.model import Dept +from backend.app.admin.model import Dept, User from backend.app.admin.schema.dept import CreateDeptParam, UpdateDeptParam from backend.common.security.permission import filter_data_permission +from backend.utils.serializers import select_join_serialize class CRUDDept(CRUDPlus[Dept]): @@ -63,8 +65,8 @@ async def get_all( if status is not None: filters['status'] = status - data_filtered = await filter_data_permission(db, request) - return await self.select_models_order(db, 'sort', 'desc', data_filtered, **filters) + data_filter = await filter_data_permission(db, request) + return await self.select_models_order(db, 'sort', 'desc', data_filter, **filters) async def create(self, db: AsyncSession, obj: CreateDeptParam) -> None: """ @@ -97,7 +99,7 @@ async def delete(self, db: AsyncSession, dept_id: int) -> int: """ return await self.delete_model_by_column(db, id=dept_id, logical_deletion=True, deleted_flag_column='del_flag') - async def get_with_relation(self, db: AsyncSession, dept_id: int) -> Dept | None: + async def get_with_relation(self, db: AsyncSession, dept_id: int) -> Any | None: """ 获取部门及关联数据 @@ -105,7 +107,12 @@ async def get_with_relation(self, db: AsyncSession, dept_id: int) -> Dept | None :param dept_id: 部门 ID :return: """ - return await self.select_model(db, dept_id, load_strategies=['users']) + result = await self.select_model( + db, + dept_id, + join_conditions=[JoinConfig(model=User, join_on=User.dept_id == self.model.id, fill_result=True)], + ) + return select_join_serialize(result, relationships=['Dept-o2m-User']) async def get_children(self, db: AsyncSession, dept_id: int) -> Sequence[Dept | None]: """ @@ -115,7 +122,7 @@ async def get_children(self, db: AsyncSession, dept_id: int) -> Sequence[Dept | :param dept_id: 部门 ID :return: """ - return await self.select_models(db, parent_id=dept_id, del_flag=0) + return await self.select_models(db, parent_id=dept_id, del_flag=False) dept_dao: CRUDDept = CRUDDept(Dept) diff --git a/backend/app/admin/service/dept_service.py b/backend/app/admin/service/dept_service.py index d75cd687..fdd62b51 100644 --- a/backend/app/admin/service/dept_service.py +++ b/backend/app/admin/service/dept_service.py @@ -68,7 +68,7 @@ async def create(*, db: AsyncSession, obj: CreateDeptParam) -> None: dept = await dept_dao.get_by_name(db, obj.name) if dept: raise errors.ConflictError(msg='部门名称已存在') - if obj.parent_id: + if obj.parent_id is not None: parent_dept = await dept_dao.get(db, obj.parent_id) if not parent_dept: raise errors.NotFoundError(msg='父级部门不存在') @@ -108,6 +108,8 @@ async def delete(*, db: AsyncSession, pk: int) -> int: :return: """ dept = await dept_dao.get_with_relation(db, pk) + if not dept: + raise errors.NotFoundError(msg='部门不存在') if dept.users: raise errors.ConflictError(msg='部门下存在用户,无法删除') children = await dept_dao.get_children(db, pk) From 5ccdc1e9c5a5e9285dcc5bedd22a08d0e154035e Mon Sep 17 00:00:00 2001 From: Wu Clan Date: Sat, 8 Nov 2025 00:08:51 +0800 Subject: [PATCH 07/25] Rename the join query functions --- backend/app/admin/crud/crud_dept.py | 2 +- backend/app/admin/crud/crud_user.py | 2 +- backend/app/admin/service/dept_service.py | 2 +- backend/app/admin/service/user_service.py | 6 +++--- backend/common/security/jwt.py | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/backend/app/admin/crud/crud_dept.py b/backend/app/admin/crud/crud_dept.py index 1fc7f124..8117899b 100644 --- a/backend/app/admin/crud/crud_dept.py +++ b/backend/app/admin/crud/crud_dept.py @@ -99,7 +99,7 @@ async def delete(self, db: AsyncSession, dept_id: int) -> int: """ return await self.delete_model_by_column(db, id=dept_id, logical_deletion=True, deleted_flag_column='del_flag') - async def get_with_relation(self, db: AsyncSession, dept_id: int) -> Any | None: + async def get_join(self, db: AsyncSession, dept_id: int) -> Any | None: """ 获取部门及关联数据 diff --git a/backend/app/admin/crud/crud_user.py b/backend/app/admin/crud/crud_user.py index 03e601cb..bee192ec 100644 --- a/backend/app/admin/crud/crud_user.py +++ b/backend/app/admin/crud/crud_user.py @@ -282,7 +282,7 @@ async def set_multi_login(self, db: AsyncSession, user_id: int, *, multi_login: """ return await self.update_model(db, user_id, {'is_multi_login': multi_login}) - async def get_joins( + async def get_join( self, db: AsyncSession, *, diff --git a/backend/app/admin/service/dept_service.py b/backend/app/admin/service/dept_service.py index fdd62b51..1e2b0fff 100644 --- a/backend/app/admin/service/dept_service.py +++ b/backend/app/admin/service/dept_service.py @@ -107,7 +107,7 @@ async def delete(*, db: AsyncSession, pk: int) -> int: :param pk: 部门 ID :return: """ - dept = await dept_dao.get_with_relation(db, pk) + dept = await dept_dao.get_join(db, pk) if not dept: raise errors.NotFoundError(msg='部门不存在') if dept.users: diff --git a/backend/app/admin/service/user_service.py b/backend/app/admin/service/user_service.py index 56c0d705..28f88b8f 100644 --- a/backend/app/admin/service/user_service.py +++ b/backend/app/admin/service/user_service.py @@ -37,7 +37,7 @@ async def get_userinfo(*, db: AsyncSession, pk: int | None = None, username: str :param username: 用户名 :return: """ - user = await user_dao.get_joins(db, user_id=pk, username=username) + user = await user_dao.get_join(db, user_id=pk, username=username) if not user: raise errors.NotFoundError(msg='用户不存在') return user @@ -51,7 +51,7 @@ async def get_roles(*, db: AsyncSession, pk: int) -> Sequence[Role]: :param pk: 用户 ID :return: """ - user = await user_dao.get_joins(db, user_id=pk) + user = await user_dao.get_join(db, user_id=pk) if not user: raise errors.NotFoundError(msg='用户不存在') return user.roles @@ -101,7 +101,7 @@ async def update(*, db: AsyncSession, pk: int, obj: UpdateUserParam) -> int: :param obj: 用户更新参数 :return: """ - user = await user_dao.get_joins(db, user_id=pk) + user = await user_dao.get_join(db, user_id=pk) if not user: raise errors.NotFoundError(msg='用户不存在') if obj.username != user.username and await user_dao.get_by_username(db, obj.username): diff --git a/backend/common/security/jwt.py b/backend/common/security/jwt.py index fc8e0b42..7c50b3de 100644 --- a/backend/common/security/jwt.py +++ b/backend/common/security/jwt.py @@ -245,7 +245,7 @@ async def get_current_user(db: AsyncSession, pk: int) -> User: """ from backend.app.admin.crud.crud_user import user_dao - user = await user_dao.get_joins(db, user_id=pk) + user = await user_dao.get_join(db, user_id=pk) if not user: raise errors.TokenError(msg='Token 无效') if not user.status: From bd0c0bc2026c7c601c72cf58da37b1ccb9eff9de Mon Sep 17 00:00:00 2001 From: Wu Clan Date: Sat, 8 Nov 2025 13:46:37 +0800 Subject: [PATCH 08/25] Update select_join_serialize doc and README --- README.md | 2 ++ README.zh-CN.md | 2 ++ backend/utils/serializers.py | 20 ++++++++++++++------ 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index b151bd24..63752f7e 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,8 @@ English | [简体中文](./README.zh-CN.md) +![star](https://github.com/fastapi-practices/fastapi_best_architecture/stargazers) + ## Pseudo 3-tier architecture The mvc architecture is a common design pattern in python web, but the 3-tier architecture is even more fascinating diff --git a/README.zh-CN.md b/README.zh-CN.md index 752adf8d..b0173ecd 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -23,6 +23,8 @@ +![star](https://github.com/fastapi-practices/fastapi_best_architecture/stargazers) + ## 伪三层架构 mvc 架构作为常规设计模式,在 python web 中也很常见,但是三层架构更令人着迷 diff --git a/backend/utils/serializers.py b/backend/utils/serializers.py index 0d3fd041..77967afa 100644 --- a/backend/utils/serializers.py +++ b/backend/utils/serializers.py @@ -92,17 +92,25 @@ def select_join_serialize( # noqa: C901 return_as_dict: bool = False, ) -> dict[str, Any] | list[dict[str, Any]] | tuple[Any, ...] | list[tuple[Any, ...]] | None: """ - 将 SQLAlchemy 连接查询结果序列化为支持属性访问的 namedtuple,支持虚拟关系嵌套 + 将 SQLAlchemy 连接查询结果序列化为字典或支持属性访问的 namedtuple - 扁平序列化:relationships=None + 扁平序列化:``relationships=None`` + | 将所有查询结果平铺到同一层级,不进行嵌套处理 + 输出:Result(name='Alice', dept=Dept(...)) - 嵌套序列化: - - row = select(User, Dept).join(...).all() - - relationships = ['User-m2o-Dept'] - - 输出:Result(name='Alice', dept=Dept(...)) + 嵌套序列化:``relationships=['User-m2o-Dept', 'User-m2m-Role', 'Role-m2m-Menu']`` + | 根据指定的关系类型将数据嵌套组织,支持层级结构 + | row = select(User, Dept, Role).join(...).all() + 输出:Result(name='Alice', dept=Dept(...), roles=[Role(..., menus=[Menu(...)])]) :param row: SQLAlchemy 查询结果 :param relationships: 表之间的虚拟关系 (source_model_class-type-target_model_class, type: o2m/m2o/o2o/m2m) + + - o2m (一对多): 目标模型类名会自动添加's'变为复数形式 (如: dept->depts) + - m2o (多对一): 目标模型类名保持单数形式 (如: user->user) + - o2o (一对一): 目标模型类名保持单数形式 (如: profile->profile) + - m2m (多对多): 目标模型类名会自动添加's'变为复数形式 (如: role->roles) + :param return_as_dict: False 返回 namedtuple,True 返回 dict :return: """ From fbaf29f8af0b956c6cbc620e021401b2ecd96eff Mon Sep 17 00:00:00 2001 From: Wu Clan Date: Sat, 8 Nov 2025 13:50:30 +0800 Subject: [PATCH 09/25] Fix typo in README --- README.md | 2 +- README.zh-CN.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 63752f7e..36fc3b9e 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ English | [简体中文](./README.zh-CN.md) -![star](https://github.com/fastapi-practices/fastapi_best_architecture/stargazers) +[![][image-star]][https://github.com/fastapi-practices/fastapi_best_architecture/stargazers] ## Pseudo 3-tier architecture diff --git a/README.zh-CN.md b/README.zh-CN.md index b0173ecd..498fd257 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -23,7 +23,7 @@ -![star](https://github.com/fastapi-practices/fastapi_best_architecture/stargazers) +[![][image-star]][https://github.com/fastapi-practices/fastapi_best_architecture/stargazers] ## 伪三层架构 From 44aca81d2998271b7058a2c64a061fbbc65e01a6 Mon Sep 17 00:00:00 2001 From: Wu Clan Date: Sat, 8 Nov 2025 14:09:19 +0800 Subject: [PATCH 10/25] Update the user delete --- README.md | 2 -- README.zh-CN.md | 2 -- backend/app/admin/crud/crud_user.py | 4 +++- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 36fc3b9e..b151bd24 100644 --- a/README.md +++ b/README.md @@ -23,8 +23,6 @@ English | [简体中文](./README.zh-CN.md) -[![][image-star]][https://github.com/fastapi-practices/fastapi_best_architecture/stargazers] - ## Pseudo 3-tier architecture The mvc architecture is a common design pattern in python web, but the 3-tier architecture is even more fascinating diff --git a/README.zh-CN.md b/README.zh-CN.md index 498fd257..752adf8d 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -23,8 +23,6 @@ -[![][image-star]][https://github.com/fastapi-practices/fastapi_best_architecture/stargazers] - ## 伪三层架构 mvc 架构作为常规设计模式,在 python web 中也很常见,但是三层架构更令人着迷 diff --git a/backend/app/admin/crud/crud_user.py b/backend/app/admin/crud/crud_user.py index bee192ec..8c0ea582 100644 --- a/backend/app/admin/crud/crud_user.py +++ b/backend/app/admin/crud/crud_user.py @@ -2,7 +2,7 @@ import bcrypt -from sqlalchemy import insert, select +from sqlalchemy import insert, select, delete from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy_crud_plus import CRUDPlus, JoinConfig @@ -174,6 +174,8 @@ async def delete(self, db: AsyncSession, user_id: int) -> int: :param user_id: 用户 ID :return: """ + user_role_stmt = delete(user_role).where(user_role.c.user_id == user_id) + await db.execute(user_role_stmt) return await self.delete_model(db, user_id) async def check_email(self, db: AsyncSession, email: str) -> User | None: From 578b9cd7b4c38bf596a1a6aa7411859cb9cf96a6 Mon Sep 17 00:00:00 2001 From: Wu Clan Date: Sat, 8 Nov 2025 14:51:22 +0800 Subject: [PATCH 11/25] Update the user social --- backend/app/admin/crud/crud_user.py | 6 ++++- backend/common/enums.py | 8 ------ backend/plugin/oauth2/api/v1/github.py | 2 +- backend/plugin/oauth2/api/v1/google.py | 2 +- backend/plugin/oauth2/api/v1/linux_do.py | 2 +- backend/plugin/oauth2/api/v1/user_social.py | 15 +++++++++++ .../plugin/oauth2/crud/crud_user_social.py | 27 +++++++++++++------ backend/plugin/oauth2/enums.py | 9 +++++++ backend/plugin/oauth2/schema/user_social.py | 2 +- .../plugin/oauth2/service/oauth2_service.py | 3 ++- backend/plugin/oauth2/service/user_social.py | 25 +++++++++++++++++ 11 files changed, 79 insertions(+), 22 deletions(-) create mode 100644 backend/plugin/oauth2/api/v1/user_social.py create mode 100644 backend/plugin/oauth2/enums.py create mode 100644 backend/plugin/oauth2/service/user_social.py diff --git a/backend/app/admin/crud/crud_user.py b/backend/app/admin/crud/crud_user.py index 8c0ea582..19149eb5 100644 --- a/backend/app/admin/crud/crud_user.py +++ b/backend/app/admin/crud/crud_user.py @@ -2,7 +2,7 @@ import bcrypt -from sqlalchemy import insert, select, delete +from sqlalchemy import delete, insert, select from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy_crud_plus import CRUDPlus, JoinConfig @@ -16,6 +16,7 @@ ) from backend.common.pagination import paging_data from backend.common.security.jwt import get_hash_password +from backend.plugin.oauth2.crud.crud_user_social import user_social_dao from backend.utils.serializers import select_join_serialize from backend.utils.timezone import timezone @@ -176,6 +177,9 @@ async def delete(self, db: AsyncSession, user_id: int) -> int: """ user_role_stmt = delete(user_role).where(user_role.c.user_id == user_id) await db.execute(user_role_stmt) + + await user_social_dao.delete_by_user_id(db, user_id) + return await self.delete_model(db, user_id) async def check_email(self, db: AsyncSession, email: str) -> User | None: diff --git a/backend/common/enums.py b/backend/common/enums.py index 174a17b1..5b1cbeac 100644 --- a/backend/common/enums.py +++ b/backend/common/enums.py @@ -103,14 +103,6 @@ class StatusType(IntEnum): enable = 1 -class UserSocialType(StrEnum): - """用户社交类型""" - - github = 'GitHub' - google = 'Google' - linux_do = 'LinuxDo' - - class FileType(StrEnum): """文件类型""" diff --git a/backend/plugin/oauth2/api/v1/github.py b/backend/plugin/oauth2/api/v1/github.py index 70226eb5..20cd0e61 100644 --- a/backend/plugin/oauth2/api/v1/github.py +++ b/backend/plugin/oauth2/api/v1/github.py @@ -5,10 +5,10 @@ from fastapi_oauth20 import FastAPIOAuth20, GitHubOAuth20 from starlette.responses import RedirectResponse -from backend.common.enums import UserSocialType from backend.common.response.response_schema import ResponseSchemaModel, response_base from backend.core.conf import settings from backend.database.db import CurrentSessionTransaction +from backend.plugin.oauth2.enums import UserSocialType from backend.plugin.oauth2.service.oauth2_service import oauth2_service router = APIRouter() diff --git a/backend/plugin/oauth2/api/v1/google.py b/backend/plugin/oauth2/api/v1/google.py index 4a649f9e..0456b921 100644 --- a/backend/plugin/oauth2/api/v1/google.py +++ b/backend/plugin/oauth2/api/v1/google.py @@ -5,10 +5,10 @@ from fastapi_oauth20 import FastAPIOAuth20, GoogleOAuth20 from starlette.responses import RedirectResponse -from backend.common.enums import UserSocialType from backend.common.response.response_schema import ResponseSchemaModel, response_base from backend.core.conf import settings from backend.database.db import CurrentSessionTransaction +from backend.plugin.oauth2.enums import UserSocialType from backend.plugin.oauth2.service.oauth2_service import oauth2_service router = APIRouter() diff --git a/backend/plugin/oauth2/api/v1/linux_do.py b/backend/plugin/oauth2/api/v1/linux_do.py index 3152584a..97f2f87b 100644 --- a/backend/plugin/oauth2/api/v1/linux_do.py +++ b/backend/plugin/oauth2/api/v1/linux_do.py @@ -5,10 +5,10 @@ from fastapi_oauth20 import FastAPIOAuth20, LinuxDoOAuth20 from starlette.responses import RedirectResponse -from backend.common.enums import UserSocialType from backend.common.response.response_schema import ResponseSchemaModel, response_base from backend.core.conf import settings from backend.database.db import CurrentSessionTransaction +from backend.plugin.oauth2.enums import UserSocialType from backend.plugin.oauth2.service.oauth2_service import oauth2_service router = APIRouter() diff --git a/backend/plugin/oauth2/api/v1/user_social.py b/backend/plugin/oauth2/api/v1/user_social.py new file mode 100644 index 00000000..576654db --- /dev/null +++ b/backend/plugin/oauth2/api/v1/user_social.py @@ -0,0 +1,15 @@ +from fastapi import APIRouter, Request + +from backend.common.response.response_schema import ResponseModel, response_base +from backend.common.security.jwt import DependsJwtAuth +from backend.database.db import CurrentSessionTransaction +from backend.plugin.oauth2.enums import UserSocialType +from backend.plugin.oauth2.service.user_social import user_social_service + +router = APIRouter() + + +@router.delete('/me', summary='解绑社交账号', dependencies=[DependsJwtAuth]) +async def unbind_user(db: CurrentSessionTransaction, request: Request, source: UserSocialType) -> ResponseModel: + await user_social_service.unbind(db=db, user_id=request.user.id, source=source) + return response_base.success() diff --git a/backend/plugin/oauth2/crud/crud_user_social.py b/backend/plugin/oauth2/crud/crud_user_social.py index d3fab440..ab7c6f7c 100644 --- a/backend/plugin/oauth2/crud/crud_user_social.py +++ b/backend/plugin/oauth2/crud/crud_user_social.py @@ -8,23 +8,23 @@ class CRUDUserSocial(CRUDPlus[UserSocial]): """用户社交账号数据库操作类""" - async def check_binding(self, db: AsyncSession, pk: int, source: str) -> UserSocial | None: + async def check_binding(self, db: AsyncSession, user_id: int, source: str) -> UserSocial | None: """ 检查系统用户社交账号绑定 :param db: 数据库会话 - :param pk: 用户 ID + :param user_id: 用户 ID :param source: 社交账号类型 :return: """ - return await self.select_model_by_column(db, user_id=pk, source=source) + return await self.select_model_by_column(db, user_id=user_id, source=source) async def get_by_sid(self, db: AsyncSession, sid: str, source: str) -> UserSocial | None: """ - 通过 UUID 获取社交用户 + 通过 sid 获取社交用户 :param db: 数据库会话 - :param sid: 第三方 UUID + :param sid: 第三方用户唯一编码 :param source: 社交账号类型 :return: """ @@ -40,15 +40,26 @@ async def create(self, db: AsyncSession, obj: CreateUserSocialParam) -> None: """ await self.create_model(db, obj) - async def delete(self, db: AsyncSession, social_id: int) -> int: + async def delete(self, db: AsyncSession, user_id: int, source: str) -> int: """ 删除用户社交账号绑定 :param db: 数据库会话 - :param social_id: 社交账号绑定 ID + :param user_id: 用户 ID + :param source: 社交账号类型 + :return: + """ + return await self.delete_model_by_column(db, user_id=user_id, source=source) + + async def delete_by_user_id(self, db: AsyncSession, user_id: int) -> int: + """ + 通过用户 ID 删除用户社交 + + :param db: 数据库会话 + :param user_id: 用户 ID :return: """ - return await self.delete_model(db, social_id) + return await self.delete_model_by_column(db, user_id=user_id) user_social_dao: CRUDUserSocial = CRUDUserSocial(UserSocial) diff --git a/backend/plugin/oauth2/enums.py b/backend/plugin/oauth2/enums.py new file mode 100644 index 00000000..abd68bd6 --- /dev/null +++ b/backend/plugin/oauth2/enums.py @@ -0,0 +1,9 @@ +from backend.common.enums import StrEnum + + +class UserSocialType(StrEnum): + """用户社交类型""" + + github = 'GitHub' + google = 'Google' + linux_do = 'LinuxDo' diff --git a/backend/plugin/oauth2/schema/user_social.py b/backend/plugin/oauth2/schema/user_social.py index c41842a4..3e2ece91 100644 --- a/backend/plugin/oauth2/schema/user_social.py +++ b/backend/plugin/oauth2/schema/user_social.py @@ -1,7 +1,7 @@ from pydantic import Field -from backend.common.enums import UserSocialType from backend.common.schema import SchemaBase +from backend.plugin.oauth2.enums import UserSocialType class UserSocialSchemaBase(SchemaBase): diff --git a/backend/plugin/oauth2/service/oauth2_service.py b/backend/plugin/oauth2/service/oauth2_service.py index a7fffc8d..cfe7f8bd 100644 --- a/backend/plugin/oauth2/service/oauth2_service.py +++ b/backend/plugin/oauth2/service/oauth2_service.py @@ -9,12 +9,13 @@ from backend.app.admin.schema.user import AddOAuth2UserParam from backend.app.admin.service.login_log_service import login_log_service from backend.common.context import ctx -from backend.common.enums import LoginLogStatusType, UserSocialType +from backend.common.enums import LoginLogStatusType from backend.common.i18n import t from backend.common.security import jwt from backend.core.conf import settings from backend.database.redis import redis_client from backend.plugin.oauth2.crud.crud_user_social import user_social_dao +from backend.plugin.oauth2.enums import UserSocialType from backend.plugin.oauth2.schema.user_social import CreateUserSocialParam from backend.utils.timezone import timezone diff --git a/backend/plugin/oauth2/service/user_social.py b/backend/plugin/oauth2/service/user_social.py new file mode 100644 index 00000000..c97f49e3 --- /dev/null +++ b/backend/plugin/oauth2/service/user_social.py @@ -0,0 +1,25 @@ +from sqlalchemy.ext.asyncio import AsyncSession + +from backend.common.exception import errors +from backend.plugin.oauth2.crud.crud_user_social import user_social_dao +from backend.plugin.oauth2.enums import UserSocialType + + +class UserSocialService: + @staticmethod + async def unbind(*, db: AsyncSession, user_id: int, source: UserSocialType) -> int: + """ + 解绑用户社交 + + :param db: 数据库绘画 + :param user_id: 用户 ID + :param source: 解绑源 + :return: + """ + bind = user_social_dao.check_binding(db, user_id, source.value) + if not bind: + raise errors.NotFoundError(msg=f'用户未绑定 {source.value} 账户') + return await user_social_dao.delete(db, user_id, source.value) + + +user_social_service: UserSocialService = UserSocialService() From a4714c51f628b7f5a9e38a6bb9a2da9dcf337e7f Mon Sep 17 00:00:00 2001 From: Wu Clan Date: Sun, 9 Nov 2025 19:56:28 +0800 Subject: [PATCH 12/25] Update the dict plugin crud --- backend/plugin/dict/crud/crud_dict_data.py | 15 +++++++-------- backend/plugin/dict/crud/crud_dict_type.py | 15 ++++++++++----- backend/plugin/dict/service/dict_type_service.py | 4 +--- 3 files changed, 18 insertions(+), 16 deletions(-) diff --git a/backend/plugin/dict/crud/crud_dict_data.py b/backend/plugin/dict/crud/crud_dict_data.py index 07467328..ca87e2ed 100644 --- a/backend/plugin/dict/crud/crud_dict_data.py +++ b/backend/plugin/dict/crud/crud_dict_data.py @@ -19,7 +19,7 @@ async def get(self, db: AsyncSession, pk: int) -> DictData | None: :param pk: 字典数据 ID :return: """ - return await self.select_model(db, pk, load_strategies={'type': 'noload'}) + return await self.select_model(db, pk) async def get_by_type_code(self, db: AsyncSession, type_code: str) -> Sequence[DictData]: """ @@ -34,7 +34,6 @@ async def get_by_type_code(self, db: AsyncSession, type_code: str) -> Sequence[D sort_columns='sort', sort_orders='desc', type_code=type_code, - load_strategies={'type': 'noload'}, ) async def get_all(self, db: AsyncSession) -> Sequence[DictData]: @@ -44,7 +43,7 @@ async def get_all(self, db: AsyncSession) -> Sequence[DictData]: :param db: 数据库会话 :return: """ - return await self.select_models(db, load_strategies={'type': 'noload'}) + return await self.select_models(db) async def get_select( self, @@ -77,7 +76,7 @@ async def get_select( if type_id is not None: filters['type_id'] = type_id - return await self.select_order('id', 'desc', load_strategies={'type': 'noload'}, **filters) + return await self.select_order('id', 'desc', **filters) async def get_by_label_and_type_code(self, db: AsyncSession, label: str, type_code: str) -> DictData | None: """ @@ -128,15 +127,15 @@ async def delete(self, db: AsyncSession, pks: list[int]) -> int: """ return await self.delete_model_by_column(db, allow_multiple=True, id__in=pks) - async def get_with_relation(self, db: AsyncSession, pk: int) -> DictData | None: + async def delete_by_type_id(self, db: AsyncSession, type_ids: list[int]) -> int: """ - 获取字典数据及关联数据 + 通过类型 ID 删除字典数据 :param db: 数据库会话 - :param pk: 字典数据 ID + :param type_ids: 字典类型 ID 列表 :return: """ - return await self.select_model(db, pk, load_strategies=['type']) + return await self.delete_model_by_column(db, allow_multiple=True, type_id__in=type_ids) dict_data_dao: CRUDDictData = CRUDDictData(DictData) diff --git a/backend/plugin/dict/crud/crud_dict_type.py b/backend/plugin/dict/crud/crud_dict_type.py index 592cf61d..bbed2dda 100644 --- a/backend/plugin/dict/crud/crud_dict_type.py +++ b/backend/plugin/dict/crud/crud_dict_type.py @@ -1,9 +1,11 @@ from collections.abc import Sequence +from typing import Any -from sqlalchemy import Select from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy_crud_plus import CRUDPlus +from backend.common.pagination import paging_data +from backend.plugin.dict.crud.crud_dict_data import dict_data_dao from backend.plugin.dict.model import DictType from backend.plugin.dict.schema.dict_type import CreateDictTypeParam, UpdateDictTypeParam @@ -28,12 +30,13 @@ async def get_all(self, db: AsyncSession) -> Sequence[DictType]: :param db: 数据库会话 :return: """ - return await self.select_models(db, load_strategies={'datas': 'noload'}) + return await self.select_models(db) - async def get_select(self, *, name: str | None, code: str | None) -> Select: + async def get_paginated(self, db: AsyncSession, name: str | None, code: str | None) -> dict[str, Any]: """ - 获取字典类型列表查询表达式 + 获取用户分页 + :param db: 数据库会话 :param name: 字典类型名称 :param code: 字典类型编码 :return: @@ -45,7 +48,8 @@ async def get_select(self, *, name: str | None, code: str | None) -> Select: if code is not None: filters['code__like'] = f'%{code}%' - return await self.select_order('id', 'desc', load_strategies={'datas': 'noload'}, **filters) + dict_type_select = await self.select_order('id', 'desc', **filters) + return await paging_data(db, dict_type_select) async def get_by_code(self, db: AsyncSession, code: str) -> DictType | None: """ @@ -86,6 +90,7 @@ async def delete(self, db: AsyncSession, pks: list[int]) -> int: :param pks: 字典类型 ID 列表 :return: """ + await dict_data_dao.delete_by_type_id(db, pks) return await self.delete_model_by_column(db, allow_multiple=True, id__in=pks) diff --git a/backend/plugin/dict/service/dict_type_service.py b/backend/plugin/dict/service/dict_type_service.py index 584e3e2a..1e840dc8 100644 --- a/backend/plugin/dict/service/dict_type_service.py +++ b/backend/plugin/dict/service/dict_type_service.py @@ -4,7 +4,6 @@ from sqlalchemy.ext.asyncio import AsyncSession from backend.common.exception import errors -from backend.common.pagination import paging_data from backend.plugin.dict.crud.crud_dict_type import dict_type_dao from backend.plugin.dict.model import DictType from backend.plugin.dict.schema.dict_type import CreateDictTypeParam, DeleteDictTypeParam, UpdateDictTypeParam @@ -49,8 +48,7 @@ async def get_list(*, db: AsyncSession, name: str | None, code: str | None) -> d :param code: 字典类型编码 :return: """ - dict_type_select = await dict_type_dao.get_select(name=name, code=code) - return await paging_data(db, dict_type_select) + return await dict_type_dao.get_paginated(db=db, name=name, code=code) @staticmethod async def create(*, db: AsyncSession, obj: CreateDictTypeParam) -> None: From 9fef37c2e57bb6874ad759d8ca2554eda880f46b Mon Sep 17 00:00:00 2001 From: Wu Clan Date: Sun, 9 Nov 2025 19:57:44 +0800 Subject: [PATCH 13/25] Update the dict plugin version --- backend/plugin/dict/plugin.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/plugin/dict/plugin.toml b/backend/plugin/dict/plugin.toml index 4c578f48..4f2a72b1 100644 --- a/backend/plugin/dict/plugin.toml +++ b/backend/plugin/dict/plugin.toml @@ -1,6 +1,6 @@ [plugin] summary = '数据字典' -version = '0.0.7' +version = '0.0.8' description = '通常用于约束前端工程数据展示' author = 'wu-clan' From 68f007f1cabddfacd93bda654357d849ef6c01f0 Mon Sep 17 00:00:00 2001 From: Wu Clan Date: Sun, 9 Nov 2025 22:07:39 +0800 Subject: [PATCH 14/25] Bump dependencies and pre-commits --- .pre-commit-config.yaml | 1 + backend/README.md | 6 +- backend/scripts/lint.sh | 2 +- pyproject.toml | 12 ++-- requirements.txt | 31 ++------- uv.lock | 151 +++++++++++----------------------------- 6 files changed, 57 insertions(+), 146 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 59d3813a..8501d2dd 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,6 +3,7 @@ repos: rev: v6.0.0 hooks: - id: end-of-file-fixer + - id: check-json - id: check-yaml - id: check-toml diff --git a/backend/README.md b/backend/README.md index 1b0955d8..53eed366 100644 --- a/backend/README.md +++ b/backend/README.md @@ -50,10 +50,10 @@ 4. Format and Lint - Auto-formatting and lint via `pre-commit` + Auto-formatting and lint via `prek` ```shell - pre-commit run --all-files + prek run --all-files ``` 5. Commit and push @@ -78,6 +78,6 @@ - `scripts/format.sh`: Perform ruff format check -- `scripts/lint.sh`: Perform pre-commit formatting +- `scripts/lint.sh`: Perform prek formatting - `scripts/export.sh`: Execute uv export dependency package diff --git a/backend/scripts/lint.sh b/backend/scripts/lint.sh index 24e4e3e9..f6f45664 100755 --- a/backend/scripts/lint.sh +++ b/backend/scripts/lint.sh @@ -1,3 +1,3 @@ #!/usr/bin/env bash -pre-commit run --all-files +prek run --all-files diff --git a/pyproject.toml b/pyproject.toml index aa84e91a..d82c4164 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,7 +17,7 @@ dependencies = [ "asyncmy>=0.2.10", "asyncpg>=0.30.0", "bcrypt>=5.0.0", - "cappa>=0.30.2", + "cappa>=0.30.4", "celery>=5.5.3", # When celery version < 6.0.0 # https://github.com/celery/celery/issues/7874 @@ -27,10 +27,10 @@ dependencies = [ "fast-captcha>=0.3.2", "fastapi-limiter>=0.1.6", "fastapi-pagination>=0.15.0", - "fastapi[standard-no-fastapi-cloud-cli]>=0.120.2", + "fastapi[standard-no-fastapi-cloud-cli]>=0.121.1", "flower>=2.0.1", "gevent>=25.9.1", - "granian>=2.5.5", + "granian>=2.5.7", "ip2loc>=1.0.0", "itsdangerous>=2.2.0", "jinja2>=3.1.6", @@ -47,7 +47,7 @@ dependencies = [ "python-socketio>=5.14.3", "redis[hiredis]>=7.0.1", "rtoml>=0.13.0", - "sqlalchemy-crud-plus>=1.13.0", + "sqlalchemy-crud-plus>=1.13.1", "sqlalchemy[asyncio]>=2.0.44", "sqlparse>=0.5.3", "starlette-context>=0.4.0", @@ -56,11 +56,11 @@ dependencies = [ [dependency-groups] dev = [ - "pytest>=8.4.0", + "pytest>=9.0.0", "pytest-sugar>=1.1.1", ] lint = [ - "pre-commit>=4.3.0", + "prek>=0.2.13", ] server = [ "aio-pika>=9.5.7", diff --git a/requirements.txt b/requirements.txt index 2d116a53..a3367bcd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -47,8 +47,6 @@ cffi==2.0.0 ; platform_python_implementation != 'PyPy' # via # cryptography # gevent -cfgv==3.4.0 - # via pre-commit click==8.3.0 # via # celery @@ -73,8 +71,6 @@ colorama==0.4.6 ; sys_platform == 'win32' # uvicorn cryptography==46.0.3 # via fastapi-best-architecture -distlib==0.4.0 - # via virtualenv dnspython==2.8.0 # via email-validator dulwich==0.24.8 @@ -89,7 +85,7 @@ exceptiongroup==1.3.0 ; python_full_version < '3.11' # pytest fast-captcha==0.3.2 # via fastapi-best-architecture -fastapi==0.121.0 +fastapi==0.121.1 # via # fastapi-best-architecture # fastapi-limiter @@ -100,8 +96,6 @@ fastapi-limiter==0.1.6 # via fastapi-best-architecture fastapi-pagination==0.15.0 # via fastapi-best-architecture -filelock==3.20.0 - # via virtualenv flower==2.0.1 # via fastapi-best-architecture gevent==25.9.1 @@ -127,8 +121,6 @@ httpx==0.28.1 # via fastapi humanize==4.14.0 # via flower -identify==2.6.15 - # via pre-commit idna==3.11 # via # anyio @@ -160,19 +152,15 @@ mdurl==0.1.2 # via markdown-it-py msgspec==0.19.0 # via fastapi-best-architecture -nodeenv==1.9.1 - # via pre-commit packaging==25.0 # via # kombu # pytest pillow==12.0.0 # via fast-captcha -platformdirs==4.5.0 - # via virtualenv pluggy==1.6.0 # via pytest -pre-commit==4.3.0 +prek==0.2.13 prometheus-client==0.23.1 # via flower prompt-toolkit==3.0.52 @@ -208,7 +196,7 @@ pygments==2.19.2 # rich pymysql==1.1.2 # via fastapi-best-architecture -pytest==8.4.2 +pytest==9.0.0 # via pytest-sugar pytest-sugar==1.1.1 python-dateutil==2.9.0.post0 @@ -228,9 +216,7 @@ python-socketio==5.14.3 pytz==2025.2 # via flower pyyaml==6.0.3 - # via - # pre-commit - # uvicorn + # via uvicorn redis==7.0.1 # via # fastapi-best-architecture @@ -246,8 +232,6 @@ rsa==4.9.1 # via python-jose rtoml==0.13.0 # via fastapi-best-architecture -setuptools==80.9.0 - # via zope-event shellingham==1.5.4 # via typer simple-websocket==1.1.0 @@ -263,7 +247,7 @@ sqlalchemy==2.0.44 # alembic # fastapi-best-architecture # sqlalchemy-crud-plus -sqlalchemy-crud-plus==1.13.0 +sqlalchemy-crud-plus==1.13.1 # via fastapi-best-architecture sqlparse==0.5.3 # via fastapi-best-architecture @@ -306,7 +290,6 @@ typing-extensions==4.15.0 # typer # typing-inspection # uvicorn - # virtualenv typing-inspection==0.4.2 # via # pydantic @@ -334,8 +317,6 @@ vine==5.1.0 # amqp # celery # kombu -virtualenv==20.35.4 - # via pre-commit watchfiles==1.1.1 # via uvicorn wcwidth==0.2.14 @@ -346,7 +327,7 @@ win32-setctime==1.2.0 ; sys_platform == 'win32' # via loguru wsproto==1.2.0 # via simple-websocket -zope-event==6.0 +zope-event==6.1 # via gevent zope-interface==8.0.1 # via gevent diff --git a/uv.lock b/uv.lock index 9441553b..b30f53af 100644 --- a/uv.lock +++ b/uv.lock @@ -418,15 +418,6 @@ wheels = [ { url = "https://mirrors.aliyun.com/pypi/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9" }, ] -[[package]] -name = "cfgv" -version = "3.4.0" -source = { registry = "https://mirrors.aliyun.com/pypi/simple" } -sdist = { url = "https://mirrors.aliyun.com/pypi/packages/11/74/539e56497d9bd1d484fd863dd69cbbfa653cd2aa27abfe35653494d85e94/cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560" } -wheels = [ - { url = "https://mirrors.aliyun.com/pypi/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9" }, -] - [[package]] name = "click" version = "8.3.0" @@ -550,15 +541,6 @@ wheels = [ { url = "https://mirrors.aliyun.com/pypi/packages/0d/c3/e90f4a4feae6410f914f8ebac129b9ae7a8c92eb60a638012dde42030a9d/cryptography-46.0.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6b5063083824e5509fdba180721d55909ffacccc8adbec85268b48439423d78c" }, ] -[[package]] -name = "distlib" -version = "0.4.0" -source = { registry = "https://mirrors.aliyun.com/pypi/simple" } -sdist = { url = "https://mirrors.aliyun.com/pypi/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d" } -wheels = [ - { url = "https://mirrors.aliyun.com/pypi/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16" }, -] - [[package]] name = "dnspython" version = "2.8.0" @@ -656,7 +638,7 @@ wheels = [ [[package]] name = "fastapi" -version = "0.121.0" +version = "0.121.1" source = { registry = "https://mirrors.aliyun.com/pypi/simple" } dependencies = [ { name = "annotated-doc" }, @@ -664,9 +646,9 @@ dependencies = [ { name = "starlette" }, { name = "typing-extensions" }, ] -sdist = { url = "https://mirrors.aliyun.com/pypi/packages/8c/e3/77a2df0946703973b9905fd0cde6172c15e0781984320123b4f5079e7113/fastapi-0.121.0.tar.gz", hash = "sha256:06663356a0b1ee93e875bbf05a31fb22314f5bed455afaaad2b2dad7f26e98fa" } +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/6b/a4/29e1b861fc9017488ed02ff1052feffa40940cb355ed632a8845df84ce84/fastapi-0.121.1.tar.gz", hash = "sha256:b6dba0538fd15dab6fe4d3e5493c3957d8a9e1e9257f56446b5859af66f32441" } wheels = [ - { url = "https://mirrors.aliyun.com/pypi/packages/dd/2c/42277afc1ba1a18f8358561eee40785d27becab8f80a1f945c0a3051c6eb/fastapi-0.121.0-py3-none-any.whl", hash = "sha256:8bdf1b15a55f4e4b0d6201033da9109ea15632cb76cf156e7b8b4019f2172106" }, + { url = "https://mirrors.aliyun.com/pypi/packages/94/fd/2e6f7d706899cc08690c5f6641e2ffbfffe019e8f16ce77104caa5730910/fastapi-0.121.1-py3-none-any.whl", hash = "sha256:2c5c7028bc3a58d8f5f09aecd3fd88a000ccc0c5ad627693264181a3c33aa1fc" }, ] [package.optional-dependencies] @@ -728,7 +710,7 @@ dev = [ { name = "pytest-sugar" }, ] lint = [ - { name = "pre-commit" }, + { name = "prek" }, ] server = [ { name = "aio-pika" }, @@ -742,18 +724,18 @@ requires-dist = [ { name = "asyncmy", specifier = ">=0.2.10" }, { name = "asyncpg", specifier = ">=0.30.0" }, { name = "bcrypt", specifier = ">=5.0.0" }, - { name = "cappa", specifier = ">=0.30.2" }, + { name = "cappa", specifier = ">=0.30.4" }, { name = "celery", specifier = ">=5.5.3" }, { name = "celery-aio-pool", specifier = ">=0.1.0rc8" }, { name = "cryptography", specifier = ">=46.0.3" }, { name = "dulwich", specifier = ">=0.24.7" }, { name = "fast-captcha", specifier = ">=0.3.2" }, - { name = "fastapi", extras = ["standard-no-fastapi-cloud-cli"], specifier = ">=0.120.2" }, + { name = "fastapi", extras = ["standard-no-fastapi-cloud-cli"], specifier = ">=0.121.1" }, { name = "fastapi-limiter", specifier = ">=0.1.6" }, { name = "fastapi-pagination", specifier = ">=0.15.0" }, { name = "flower", specifier = ">=2.0.1" }, { name = "gevent", specifier = ">=25.9.1" }, - { name = "granian", specifier = ">=2.5.5" }, + { name = "granian", specifier = ">=2.5.7" }, { name = "ip2loc", specifier = ">=1.0.0" }, { name = "itsdangerous", specifier = ">=2.2.0" }, { name = "jinja2", specifier = ">=3.1.6" }, @@ -770,7 +752,7 @@ requires-dist = [ { name = "redis", extras = ["hiredis"], specifier = ">=7.0.1" }, { name = "rtoml", specifier = ">=0.13.0" }, { name = "sqlalchemy", extras = ["asyncio"], specifier = ">=2.0.44" }, - { name = "sqlalchemy-crud-plus", specifier = ">=1.13.0" }, + { name = "sqlalchemy-crud-plus", specifier = ">=1.13.1" }, { name = "sqlparse", specifier = ">=0.5.3" }, { name = "starlette-context", specifier = ">=0.4.0" }, { name = "user-agents", specifier = ">=2.2.0" }, @@ -778,10 +760,10 @@ requires-dist = [ [package.metadata.requires-dev] dev = [ - { name = "pytest", specifier = ">=8.4.0" }, + { name = "pytest", specifier = ">=9.0.0" }, { name = "pytest-sugar", specifier = ">=1.1.1" }, ] -lint = [{ name = "pre-commit", specifier = ">=4.3.0" }] +lint = [{ name = "prek", specifier = ">=0.2.13" }] server = [ { name = "aio-pika", specifier = ">=9.5.7" }, { name = "wait-for-it", specifier = ">=2.3.0" }, @@ -833,15 +815,6 @@ wheels = [ { url = "https://mirrors.aliyun.com/pypi/packages/68/91/b835e07234170ba85473227aa107bcf1dc616ff6cb643c0bd9b8225a55f1/fastapi_pagination-0.15.0-py3-none-any.whl", hash = "sha256:ffef937e78903fcb6f356b8407ec1fb0620a06675087fa7d0c4e537a60aa0447" }, ] -[[package]] -name = "filelock" -version = "3.20.0" -source = { registry = "https://mirrors.aliyun.com/pypi/simple" } -sdist = { url = "https://mirrors.aliyun.com/pypi/packages/58/46/0028a82567109b5ef6e4d2a1f04a583fb513e6cf9527fcdd09afd817deeb/filelock-3.20.0.tar.gz", hash = "sha256:711e943b4ec6be42e1d4e6690b48dc175c822967466bb31c0c293f34334c13f4" } -wheels = [ - { url = "https://mirrors.aliyun.com/pypi/packages/76/91/7216b27286936c16f5b4d0c530087e4a54eead683e6b0b73dd0c64844af6/filelock-3.20.0-py3-none-any.whl", hash = "sha256:339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2" }, -] - [[package]] name = "flower" version = "2.0.1" @@ -1239,15 +1212,6 @@ wheels = [ { url = "https://mirrors.aliyun.com/pypi/packages/c3/5b/9512c5fb6c8218332b530f13500c6ff5f3ce3342f35e0dd7be9ac3856fd3/humanize-4.14.0-py3-none-any.whl", hash = "sha256:d57701248d040ad456092820e6fde56c930f17749956ac47f4f655c0c547bfff" }, ] -[[package]] -name = "identify" -version = "2.6.15" -source = { registry = "https://mirrors.aliyun.com/pypi/simple" } -sdist = { url = "https://mirrors.aliyun.com/pypi/packages/ff/e7/685de97986c916a6d93b3876139e00eef26ad5bbbd61925d670ae8013449/identify-2.6.15.tar.gz", hash = "sha256:e4f4864b96c6557ef2a1e1c951771838f4edc9df3a72ec7118b338801b11c7bf" } -wheels = [ - { url = "https://mirrors.aliyun.com/pypi/packages/0f/1c/e5fd8f973d4f375adb21565739498e2e9a1e54c858a97b9a8ccfdc81da9b/identify-2.6.15-py2.py3-none-any.whl", hash = "sha256:1181ef7608e00704db228516541eb83a88a9f94433a8c80bb9b5bd54b1d81757" }, -] - [[package]] name = "idna" version = "3.11" @@ -1616,15 +1580,6 @@ wheels = [ { url = "https://mirrors.aliyun.com/pypi/packages/b7/da/7d22601b625e241d4f23ef1ebff8acfc60da633c9e7e7922e24d10f592b3/multidict-6.7.0-py3-none-any.whl", hash = "sha256:394fc5c42a333c9ffc3e421a4c85e08580d990e08b99f6bf35b4132114c5dcb3" }, ] -[[package]] -name = "nodeenv" -version = "1.9.1" -source = { registry = "https://mirrors.aliyun.com/pypi/simple" } -sdist = { url = "https://mirrors.aliyun.com/pypi/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f" } -wheels = [ - { url = "https://mirrors.aliyun.com/pypi/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9" }, -] - [[package]] name = "packaging" version = "25.0" @@ -1741,15 +1696,6 @@ wheels = [ { url = "https://mirrors.aliyun.com/pypi/packages/95/7e/f896623c3c635a90537ac093c6a618ebe1a90d87206e42309cb5d98a1b9e/pillow-12.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:b290fd8aa38422444d4b50d579de197557f182ef1068b75f5aa8558638b8d0a5" }, ] -[[package]] -name = "platformdirs" -version = "4.5.0" -source = { registry = "https://mirrors.aliyun.com/pypi/simple" } -sdist = { url = "https://mirrors.aliyun.com/pypi/packages/61/33/9611380c2bdb1225fdef633e2a9610622310fed35ab11dac9620972ee088/platformdirs-4.5.0.tar.gz", hash = "sha256:70ddccdd7c99fc5942e9fc25636a8b34d04c24b335100223152c2803e4063312" } -wheels = [ - { url = "https://mirrors.aliyun.com/pypi/packages/73/cb/ac7874b3e5d58441674fb70742e6c374b28b0c7cb988d37d991cde47166c/platformdirs-4.5.0-py3-none-any.whl", hash = "sha256:e578a81bb873cbb89a41fcc904c7ef523cc18284b7e3b3ccf06aca1403b7ebd3" }, -] - [[package]] name = "pluggy" version = "1.6.0" @@ -1760,19 +1706,29 @@ wheels = [ ] [[package]] -name = "pre-commit" -version = "4.3.0" +name = "prek" +version = "0.2.13" source = { registry = "https://mirrors.aliyun.com/pypi/simple" } -dependencies = [ - { name = "cfgv" }, - { name = "identify" }, - { name = "nodeenv" }, - { name = "pyyaml" }, - { name = "virtualenv" }, -] -sdist = { url = "https://mirrors.aliyun.com/pypi/packages/ff/29/7cf5bbc236333876e4b41f56e06857a87937ce4bf91e117a6991a2dbb02a/pre_commit-4.3.0.tar.gz", hash = "sha256:499fe450cc9d42e9d58e606262795ecb64dd05438943c62b66f6a8673da30b16" } +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/f8/57/a0d9d2d39c8477ae44096f9c8b962395428309019875d65e130f60478015/prek-0.2.13.tar.gz", hash = "sha256:eca64c201938cd71ca09eec9b3e31ad031a251f4aa22a5132eb1c1640d86114f" } wheels = [ - { url = "https://mirrors.aliyun.com/pypi/packages/5b/a5/987a405322d78a73b66e39e4a90e4ef156fd7141bf71df987e50717c321b/pre_commit-4.3.0-py2.py3-none-any.whl", hash = "sha256:2b0747ad7e6e967169136edffee14c16e148a778a54e4f967921aa1ebf2308d8" }, + { url = "https://mirrors.aliyun.com/pypi/packages/29/64/51372417ffdbef0cc41ed2fa22be064fe247246fff364dbc86ac43364fe2/prek-0.2.13-py3-none-linux_armv6l.whl", hash = "sha256:9f55198e4b0f08c544e790184898459973755d5b83eb21e3527d789f3fc6855e" }, + { url = "https://mirrors.aliyun.com/pypi/packages/80/f3/6375bd1b4e4786d26329613a115284ec9d363ccf366236764c168a81a2cf/prek-0.2.13-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:a8724e40242a4ad3cb3f14d17764c287d9b0a5ea61ac91a861e5a676bfe21c99" }, + { url = "https://mirrors.aliyun.com/pypi/packages/c7/9a/75770f16044b6cf1f07bdaa9a7966adbf09919531e86d890964799194397/prek-0.2.13-py3-none-macosx_11_0_arm64.whl", hash = "sha256:dd5e0ddc4e4c72633ee7d400c3be7df53b7bb0e61ba7c9ea4cb490d476824f79" }, + { url = "https://mirrors.aliyun.com/pypi/packages/6b/05/91c44cb60758d3c781ea057b70b59e1870c667cfb64d12e636f54d364538/prek-0.2.13-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:8a869f9c98da675efc947e0aa4c7a8d8434d57fa5bad20e27e54855160771053" }, + { url = "https://mirrors.aliyun.com/pypi/packages/0c/7c/41ada766ca3a5cd5f286222ae30b909edac5cb95f076f6af2ff40f3cc3b6/prek-0.2.13-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0f5eac126203c89e38062e665f1427207dece178f9e19bacbc37056ab8504148" }, + { url = "https://mirrors.aliyun.com/pypi/packages/e9/ee/1017ae7207e3542a7ecfbabf2dab910c7691996956fcc124f94c1219ae1c/prek-0.2.13-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8cea9a0a46fc5e62ebf4cde1e700993737b6828228cf60f1ccfa1c91092e7e7f" }, + { url = "https://mirrors.aliyun.com/pypi/packages/22/4a/ff74278b426cda5dee5d6280dc87fd47f428693d3fadb98a5fbfb0f2b329/prek-0.2.13-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:dde77f9ee904df60fa9cecfbfde04257d26c3f5d0c5ee55211738b3990ff740f" }, + { url = "https://mirrors.aliyun.com/pypi/packages/b3/91/e4e6eddaae3bb794ca1f98948e10c8137da7523a0d728253522a1e4a83f6/prek-0.2.13-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8ae60b5d753272c1051667a257cbb13cfb197ef32900aee4cefa98352d5e7576" }, + { url = "https://mirrors.aliyun.com/pypi/packages/8d/4f/2d3d707d93bf4f48b8b95d4bcafe90df7d4f99b147442b7bbaac5e7f9838/prek-0.2.13-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e7cb2a54d55c35b57548fc1cb74fb66126ed1437a06a33416c6484e0eb4dd80e" }, + { url = "https://mirrors.aliyun.com/pypi/packages/61/e5/b44f3ee2d75f1c0a2a844e3a0980ba712bca5adefb4c65942f85e22156eb/prek-0.2.13-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b7d0c1b1d6e315dd51cdb4e68957d5ef38a67e2c5f0dab53168cb6e842539bbf" }, + { url = "https://mirrors.aliyun.com/pypi/packages/1d/a0/0d366c35eeca0f40080f9afa4f7a0bdf4b4103a3d0ece7965a41e3e56986/prek-0.2.13-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:06dc43f2d7219f2bad2c01085ad444de517b5d28e5ef361274ff39a46b68f2cc" }, + { url = "https://mirrors.aliyun.com/pypi/packages/5d/54/7c5dac73f2f825ef54f61b9c96f7a6c8369b17746b208b007b7e50a86a54/prek-0.2.13-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:e206979c3d1834fc1683c79e8c72b7e18be3923ca5695de0642d0c9d23e2010a" }, + { url = "https://mirrors.aliyun.com/pypi/packages/b9/de/451fdfdb07e40274998127166683ab828d4940f16ba2f3ceaa074b2bf1ae/prek-0.2.13-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:fa3667c641b77c9cb100d428e615f69cf45018615af32b8c63bb4fa2cbbb8769" }, + { url = "https://mirrors.aliyun.com/pypi/packages/d6/fe/47edb36d854fafcad8548fa847986cf5acb7c82c8d00ced41bd3c36a4d97/prek-0.2.13-py3-none-musllinux_1_1_i686.whl", hash = "sha256:82555ede81a7ca058ffe450e0cf7aab85db2063aeeb00d4b1704b32ccb3a4e23" }, + { url = "https://mirrors.aliyun.com/pypi/packages/94/db/dce44c94ee50514053996f8f9b36f11b07bdf8beb67c2420b7a36b3cafb5/prek-0.2.13-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:a72f909dd737aeda07d3768aab39d8ed2574ddf05842b8a324434810d2e7160f" }, + { url = "https://mirrors.aliyun.com/pypi/packages/bc/c4/56fa24632dac132e08ce1f2a68fb27ac4c4463a19c79e902acfbf12ecc4d/prek-0.2.13-py3-none-win32.whl", hash = "sha256:beb5cffb1575b645022305a601bdd395b5b005c42368fedb34bfc6aebed24b36" }, + { url = "https://mirrors.aliyun.com/pypi/packages/ab/5a/0ac93e96da38a61ae1c116a819b66520cfcb252e3c790a2726a21cefbb90/prek-0.2.13-py3-none-win_amd64.whl", hash = "sha256:75fe11e6689431b5a3f818276dfbcbb3502cd2a4b07a3efaf3460204adaa9a89" }, + { url = "https://mirrors.aliyun.com/pypi/packages/05/de/8832527ecce043cdfe28cd6a5cb580c79e64f2abe1099f87e03a71614060/prek-0.2.13-py3-none-win_arm64.whl", hash = "sha256:6b3b0e07a2da4e67e7110399b5dbd8d9205df8ff39fbe80bd37ffa194c639990" }, ] [[package]] @@ -2200,7 +2156,7 @@ wheels = [ [[package]] name = "pytest" -version = "8.4.2" +version = "9.0.0" source = { registry = "https://mirrors.aliyun.com/pypi/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, @@ -2211,9 +2167,9 @@ dependencies = [ { name = "pygments" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://mirrors.aliyun.com/pypi/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01" } +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/da/1d/eb34f286b164c5e431a810a38697409cca1112cee04b287bb56ac486730b/pytest-9.0.0.tar.gz", hash = "sha256:8f44522eafe4137b0f35c9ce3072931a788a21ee40a2ed279e817d3cc16ed21e" } wheels = [ - { url = "https://mirrors.aliyun.com/pypi/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79" }, + { url = "https://mirrors.aliyun.com/pypi/packages/72/99/cafef234114a3b6d9f3aaed0723b437c40c57bdb7b3e4c3a575bc4890052/pytest-9.0.0-py3-none-any.whl", hash = "sha256:e5ccdf10b0bac554970ee88fc1a4ad0ee5d221f8ef22321f9b7e4584e19d7f96" }, ] [[package]] @@ -2499,15 +2455,6 @@ wheels = [ { url = "https://mirrors.aliyun.com/pypi/packages/7a/63/48fd3207eb8f50566d871d86ea25cd86e4f2de2459229907e271272f58ac/rtoml-0.13.0-cp314-cp314-win_arm64.whl", hash = "sha256:f513e54f6788038bb6473564544b27cecd48dc2666fc066eb09f3759df4e3b42" }, ] -[[package]] -name = "setuptools" -version = "80.9.0" -source = { registry = "https://mirrors.aliyun.com/pypi/simple" } -sdist = { url = "https://mirrors.aliyun.com/pypi/packages/18/5d/3bf57dcd21979b887f014ea83c24ae194cfcd12b9e0fda66b957c69d1fca/setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c" } -wheels = [ - { url = "https://mirrors.aliyun.com/pypi/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922" }, -] - [[package]] name = "shellingham" version = "1.5.4" @@ -2599,15 +2546,15 @@ asyncio = [ [[package]] name = "sqlalchemy-crud-plus" -version = "1.13.0" +version = "1.13.1" source = { registry = "https://mirrors.aliyun.com/pypi/simple" } dependencies = [ { name = "pydantic" }, { name = "sqlalchemy" }, ] -sdist = { url = "https://mirrors.aliyun.com/pypi/packages/18/83/cd0d859a483660ca9301a5e339b156c42569ccf2052bfb9adacc0ea601a0/sqlalchemy_crud_plus-1.13.0.tar.gz", hash = "sha256:3e6c244ec834c609d6bcd886b5a070ce06c0853b0a82825f5c7dbac65f8bfc1d" } +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/f1/64/38823847cf4b324e0a856b5e0f7978dcc7b1ab10b960e47f04da7f132ef1/sqlalchemy_crud_plus-1.13.1.tar.gz", hash = "sha256:75f538e47ef2884f0e06c5b85ba2590b5c104afaff31db73e2e1614916d77699" } wheels = [ - { url = "https://mirrors.aliyun.com/pypi/packages/a1/fe/39895d08a65f9c2a8029d5a4223d9cbcb8f6d519264929f38e456ebd34bc/sqlalchemy_crud_plus-1.13.0-py3-none-any.whl", hash = "sha256:5f8dd7ca1a3fc9b629dec71cab26c6f2f056c9c7bf1e83915128e746527b6a89" }, + { url = "https://mirrors.aliyun.com/pypi/packages/24/81/3a0ba07ed4a1714a3fd575c4ac9351ae1fb5ae7d77f3ba39d3302b830b9b/sqlalchemy_crud_plus-1.13.1-py3-none-any.whl", hash = "sha256:d5b0d76dffd1d0060924123cafc64062cdc4ffbf6894d336a45fc243354137ba" }, ] [[package]] @@ -2897,21 +2844,6 @@ wheels = [ { url = "https://mirrors.aliyun.com/pypi/packages/03/ff/7c0c86c43b3cbb927e0ccc0255cb4057ceba4799cd44ae95174ce8e8b5b2/vine-5.1.0-py3-none-any.whl", hash = "sha256:40fdf3c48b2cfe1c38a49e9ae2da6fda88e4794c810050a728bd7413811fb1dc" }, ] -[[package]] -name = "virtualenv" -version = "20.35.4" -source = { registry = "https://mirrors.aliyun.com/pypi/simple" } -dependencies = [ - { name = "distlib" }, - { name = "filelock" }, - { name = "platformdirs" }, - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://mirrors.aliyun.com/pypi/packages/20/28/e6f1a6f655d620846bd9df527390ecc26b3805a0c5989048c210e22c5ca9/virtualenv-20.35.4.tar.gz", hash = "sha256:643d3914d73d3eeb0c552cbb12d7e82adf0e504dbf86a3182f8771a153a1971c" } -wheels = [ - { url = "https://mirrors.aliyun.com/pypi/packages/79/0c/c05523fa3181fdf0c9c52a6ba91a23fbf3246cc095f26f6516f9c60e6771/virtualenv-20.35.4-py3-none-any.whl", hash = "sha256:c21c9cede36c9753eeade68ba7d523529f228a403463376cf821eaae2b650f1b" }, -] - [[package]] name = "wait-for-it" version = "2.3.0" @@ -3244,14 +3176,11 @@ wheels = [ [[package]] name = "zope-event" -version = "6.0" +version = "6.1" source = { registry = "https://mirrors.aliyun.com/pypi/simple" } -dependencies = [ - { name = "setuptools" }, -] -sdist = { url = "https://mirrors.aliyun.com/pypi/packages/c2/d8/9c8b0c6bb1db09725395618f68d3b8a08089fca0aed28437500caaf713ee/zope_event-6.0.tar.gz", hash = "sha256:0ebac894fa7c5f8b7a89141c272133d8c1de6ddc75ea4b1f327f00d1f890df92" } +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/46/33/d3eeac228fc14de76615612ee208be2d8a5b5b0fada36bf9b62d6b40600c/zope_event-6.1.tar.gz", hash = "sha256:6052a3e0cb8565d3d4ef1a3a7809336ac519bc4fe38398cb8d466db09adef4f0" } wheels = [ - { url = "https://mirrors.aliyun.com/pypi/packages/d1/b5/1abb5a8b443314c978617bf46d5d9ad648bdf21058074e817d7efbb257db/zope_event-6.0-py3-none-any.whl", hash = "sha256:6f0922593407cc673e7d8766b492c519f91bdc99f3080fe43dcec0a800d682a3" }, + { url = "https://mirrors.aliyun.com/pypi/packages/c2/b0/956902e5e1302f8c5d124e219c6bf214e2649f92ad5fce85b05c039a04c9/zope_event-6.1-py3-none-any.whl", hash = "sha256:0ca78b6391b694272b23ec1335c0294cc471065ed10f7f606858fc54566c25a0" }, ] [[package]] From 759db21dec9bcf1e2f1aa2fd64d760574f297319 Mon Sep 17 00:00:00 2001 From: Wu Clan Date: Sun, 9 Nov 2025 22:11:49 +0800 Subject: [PATCH 15/25] Update the code generator plugin crud --- backend/plugin/code_generator/crud/crud_business.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/plugin/code_generator/crud/crud_business.py b/backend/plugin/code_generator/crud/crud_business.py index 45fe8cea..c402815a 100644 --- a/backend/plugin/code_generator/crud/crud_business.py +++ b/backend/plugin/code_generator/crud/crud_business.py @@ -52,7 +52,7 @@ async def get_select(self, table_name: str | None) -> Select: if table_name is not None: filters['table_name__like'] = f'%{table_name}%' - return await self.select_order('id', 'desc', load_strategies={'gen_column': 'noload'}, **filters) + return await self.select_order('id', 'desc', **filters) async def create(self, db: AsyncSession, obj: CreateGenBusinessParam) -> None: """ From bea36914b37fd8b93484bcf95c9894d0759300b4 Mon Sep 17 00:00:00 2001 From: Wu Clan Date: Sun, 9 Nov 2025 22:54:29 +0800 Subject: [PATCH 16/25] Update the menu crud --- backend/app/admin/crud/crud_menu.py | 10 +++++++--- backend/app/admin/service/menu_service.py | 9 ++++++--- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/backend/app/admin/crud/crud_menu.py b/backend/app/admin/crud/crud_menu.py index dd712a40..3764511b 100644 --- a/backend/app/admin/crud/crud_menu.py +++ b/backend/app/admin/crud/crud_menu.py @@ -1,9 +1,11 @@ from collections.abc import Sequence +from sqlalchemy import delete from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy_crud_plus import CRUDPlus from backend.app.admin.model import Menu +from backend.app.admin.model.m2m import role_menu from backend.app.admin.schema.menu import CreateMenuParam, UpdateMenuParam @@ -92,9 +94,12 @@ async def delete(self, db: AsyncSession, menu_id: int) -> int: :param menu_id: 菜单 ID :return: """ + role_menu_stmt = delete(role_menu).where(role_menu.c.menu_id == menu_id) + await db.execute(role_menu_stmt) + return await self.delete_model(db, menu_id) - async def get_children(self, db: AsyncSession, menu_id: int) -> list[Menu | None]: + async def get_children(self, db: AsyncSession, menu_id: int) -> Sequence[Menu | None]: """ 获取子菜单列表 @@ -102,8 +107,7 @@ async def get_children(self, db: AsyncSession, menu_id: int) -> list[Menu | None :param menu_id: 菜单 ID :return: """ - menu = await self.select_model(db, menu_id, load_strategies=['children']) - return menu.children + return await self.select_models(db, parent_id=menu_id) menu_dao: CRUDMenu = CRUDMenu(Menu) diff --git a/backend/app/admin/service/menu_service.py b/backend/app/admin/service/menu_service.py index 9ce705a8..35a9c346 100644 --- a/backend/app/admin/service/menu_service.py +++ b/backend/app/admin/service/menu_service.py @@ -54,7 +54,7 @@ async def get_sidebar(*, db: AsyncSession, request: Request) -> list[dict[str, A :param request: FastAPI 请求对象 :return: """ - + menu_data = None if request.user.is_superuser: menu_data = await menu_dao.get_sidebar(db, None) else: @@ -64,8 +64,11 @@ async def get_sidebar(*, db: AsyncSession, request: Request) -> list[dict[str, A for role in roles: menu_ids.update(menu.id for menu in role.menus) menu_data = await menu_dao.get_sidebar(db, list(menu_ids)) - menu_tree = get_vben5_tree_data(menu_data) - return menu_tree + + if menu_data: + return get_vben5_tree_data(menu_data) + + return [] @staticmethod async def create(*, db: AsyncSession, obj: CreateMenuParam) -> None: From 3a693822ac80bcf4317666e5a9d625a369ba479d Mon Sep 17 00:00:00 2001 From: Wu Clan Date: Sun, 9 Nov 2025 23:19:38 +0800 Subject: [PATCH 17/25] Update the role crud --- backend/app/admin/crud/crud_role.py | 74 +++++++++++++------ backend/app/admin/schema/role.py | 14 ++++ .../app/admin/service/data_scope_service.py | 2 + backend/app/admin/service/menu_service.py | 2 + backend/app/admin/service/role_service.py | 8 +- 5 files changed, 73 insertions(+), 27 deletions(-) diff --git a/backend/app/admin/crud/crud_role.py b/backend/app/admin/crud/crud_role.py index dd859262..bb86a46d 100644 --- a/backend/app/admin/crud/crud_role.py +++ b/backend/app/admin/crud/crud_role.py @@ -1,16 +1,22 @@ from collections.abc import Sequence +from typing import Any -from sqlalchemy import Select, select +from sqlalchemy import delete, insert from sqlalchemy.ext.asyncio import AsyncSession -from sqlalchemy_crud_plus import CRUDPlus +from sqlalchemy_crud_plus import CRUDPlus, JoinConfig from backend.app.admin.model import DataScope, Menu, Role +from backend.app.admin.model.m2m import role_data_scope, role_menu from backend.app.admin.schema.role import ( + CreateRoleMenuParam, CreateRoleParam, + CreateRoleScopeParam, UpdateRoleMenuParam, UpdateRoleParam, UpdateRoleScopeParam, ) +from backend.common.pagination import paging_data +from backend.utils.serializers import select_join_serialize class CRUDRole(CRUDPlus[Role]): @@ -26,7 +32,7 @@ async def get(self, db: AsyncSession, role_id: int) -> Role | None: """ return await self.select_model(db, role_id) - async def get_with_relation(self, db: AsyncSession, role_id: int) -> Role | None: + async def get_with_relation(self, db: AsyncSession, role_id: int) -> Any: """ 获取角色及关联数据 @@ -34,7 +40,18 @@ async def get_with_relation(self, db: AsyncSession, role_id: int) -> Role | None :param role_id: 角色 ID :return: """ - return await self.select_model(db, role_id, load_strategies=['menus', 'scopes']) + result = await self.select_models( + db, + id=role_id, + join_conditions=[ + JoinConfig(model=role_menu, join_on=role_menu.c.role_id == self.model.id), + JoinConfig(model=Menu, join_on=Menu.id == role_menu.c.menu_id, fill_result=True), + JoinConfig(model=role_data_scope, join_on=role_data_scope.c.role_id == self.model.id), + JoinConfig(model=DataScope, join_on=DataScope.id == role_data_scope.c.data_scope_id, fill_result=True), + ], + ) + + return select_join_serialize(result, relationships=['Role-m2m-Menu', 'Role-m2m-DataScope']) async def get_all(self, db: AsyncSession) -> Sequence[Role]: """ @@ -45,10 +62,11 @@ async def get_all(self, db: AsyncSession) -> Sequence[Role]: """ return await self.select_models(db) - async def get_select(self, name: str | None, status: int | None) -> Select: + async def get_paginated(self, db: AsyncSession, name: str | None, status: int | None) -> dict[str, Any]: """ - 获取角色列表查询表达式 + 获取角色分页 + :param db: 数据库会话 :param name: 角色名称 :param status: 角色状态 :return: @@ -61,15 +79,11 @@ async def get_select(self, name: str | None, status: int | None) -> Select: if status is not None: filters['status'] = status - return await self.select_order( + role_select = await self.select_order( 'id', - load_strategies={ - 'users': 'noload', - 'menus': 'noload', - 'scopes': 'noload', - }, **filters, ) + return await paging_data(db, role_select) async def get_by_name(self, db: AsyncSession, name: str) -> Role | None: """ @@ -102,7 +116,8 @@ async def update(self, db: AsyncSession, role_id: int, obj: UpdateRoleParam) -> """ return await self.update_model(db, role_id, obj) - async def update_menus(self, db: AsyncSession, role_id: int, menu_ids: UpdateRoleMenuParam) -> int: + @staticmethod + async def update_menus(db: AsyncSession, role_id: int, menu_ids: UpdateRoleMenuParam) -> int: """ 更新角色菜单 @@ -111,13 +126,19 @@ async def update_menus(self, db: AsyncSession, role_id: int, menu_ids: UpdateRol :param menu_ids: 菜单 ID 列表 :return: """ - current_role = await self.get_with_relation(db, role_id) - stmt = select(Menu).where(Menu.id.in_(menu_ids.menus)) - menus = await db.execute(stmt) - current_role.menus = menus.scalars().all() - return len(current_role.menus) + role_menu_stmt = delete(role_menu).where(role_menu.c.role_id == role_id) + await db.execute(role_menu_stmt) + + role_menu_data = [ + CreateRoleMenuParam(role_id=role_id, menu_id=menu_id).model_dump() for menu_id in menu_ids.menus + ] + role_menu_stmt = insert(role_menu) + await db.execute(role_menu_stmt, role_menu_data) - async def update_scopes(self, db: AsyncSession, role_id: int, scope_ids: UpdateRoleScopeParam) -> int: + return len(menu_ids.menus) + + @staticmethod + async def update_scopes(db: AsyncSession, role_id: int, scope_ids: UpdateRoleScopeParam) -> int: """ 更新角色数据范围 @@ -126,11 +147,16 @@ async def update_scopes(self, db: AsyncSession, role_id: int, scope_ids: UpdateR :param scope_ids: 权限范围 ID 列表 :return: """ - current_role = await self.get_with_relation(db, role_id) - stmt = select(DataScope).where(DataScope.id.in_(scope_ids.scopes)) - scopes = await db.execute(stmt) - current_role.scopes = scopes.scalars().all() - return len(current_role.scopes) + role_scope_stmt = delete(role_data_scope).where(role_data_scope.c.role_id == role_id) + await db.execute(role_scope_stmt) + + role_scope_data = [ + CreateRoleScopeParam(role_id=role_id, data_scope_id=scope_id).model_dump() for scope_id in scope_ids.scopes + ] + role_scope_stmt = insert(role_data_scope) + await db.execute(role_scope_stmt, role_scope_data) + + return len(scope_ids.scopes) async def delete(self, db: AsyncSession, role_ids: list[int]) -> int: """ diff --git a/backend/app/admin/schema/role.py b/backend/app/admin/schema/role.py index 922af9e0..8f6be43d 100644 --- a/backend/app/admin/schema/role.py +++ b/backend/app/admin/schema/role.py @@ -31,12 +31,26 @@ class DeleteRoleParam(SchemaBase): pks: list[int] = Field(description='角色 ID 列表') +class CreateRoleMenuParam(SchemaBase): + """创建角色菜单参数""" + + role_id: int = Field(description='角色 ID') + menu_id: int = Field(description='菜单 ID') + + class UpdateRoleMenuParam(SchemaBase): """更新角色菜单参数""" menus: list[int] = Field(description='菜单 ID 列表') +class CreateRoleScopeParam(SchemaBase): + """创建角色数据范围参数""" + + role_id: int = Field(description='角色 ID') + data_scope_id: int = Field(description='数据范围 ID') + + class UpdateRoleScopeParam(SchemaBase): """更新角色数据范围参数""" diff --git a/backend/app/admin/service/data_scope_service.py b/backend/app/admin/service/data_scope_service.py index 54ad2ad7..9ba0bc47 100644 --- a/backend/app/admin/service/data_scope_service.py +++ b/backend/app/admin/service/data_scope_service.py @@ -105,6 +105,7 @@ async def update(*, db: AsyncSession, pk: int, obj: UpdateDataScopeParam) -> int if data_scope.name != obj.name and await data_scope_dao.get_by_name(db, obj.name): raise errors.ConflictError(msg='数据范围已存在') count = await data_scope_dao.update(db, pk, obj) + # TODO: 重构缓存清理 for role in await data_scope.awaitable_attrs.roles: for user in await role.awaitable_attrs.users: await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}') @@ -132,6 +133,7 @@ async def delete(*, db: AsyncSession, obj: DeleteDataScopeParam) -> int: :return: """ count = await data_scope_dao.delete(db, obj.pks) + # TODO: 重构缓存清理 for pk in obj.pks: data_rule = await data_scope_dao.get(db, pk) if data_rule: diff --git a/backend/app/admin/service/menu_service.py b/backend/app/admin/service/menu_service.py index 35a9c346..6296ba3d 100644 --- a/backend/app/admin/service/menu_service.py +++ b/backend/app/admin/service/menu_service.py @@ -112,6 +112,7 @@ async def update(*, db: AsyncSession, pk: int, obj: UpdateMenuParam) -> int: if obj.parent_id == menu.id: raise errors.ForbiddenError(msg='禁止关联自身为父级') count = await menu_dao.update(db, pk, obj) + # TODO: 重构缓存清理 for role in await menu.awaitable_attrs.roles: for user in await role.awaitable_attrs.users: await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}') @@ -132,6 +133,7 @@ async def delete(*, db: AsyncSession, pk: int) -> int: raise errors.ConflictError(msg='菜单下存在子菜单,无法删除') menu = await menu_dao.get(db, pk) count = await menu_dao.delete(db, pk) + # TODO: 重构缓存清理 if menu: for role in await menu.awaitable_attrs.roles: for user in await role.awaitable_attrs.users: diff --git a/backend/app/admin/service/role_service.py b/backend/app/admin/service/role_service.py index ab750c79..268f0c17 100644 --- a/backend/app/admin/service/role_service.py +++ b/backend/app/admin/service/role_service.py @@ -15,7 +15,6 @@ UpdateRoleScopeParam, ) from backend.common.exception import errors -from backend.common.pagination import paging_data from backend.core.conf import settings from backend.database.redis import redis_client from backend.utils.build_tree import get_tree_data @@ -61,8 +60,7 @@ async def get_list(*, db: AsyncSession, name: str | None, status: int | None) -> :param status: 状态 :return: """ - role_select = await role_dao.get_select(name=name, status=status) - return await paging_data(db, role_select) + return await role_dao.get_paginated(db=db, name=name, status=status) @staticmethod async def get_menu_tree(*, db: AsyncSession, pk: int) -> list[dict[str, Any] | None]: @@ -128,6 +126,7 @@ async def update(*, db: AsyncSession, pk: int, obj: UpdateRoleParam) -> int: if role.name != obj.name and await role_dao.get_by_name(db, obj.name): raise errors.ConflictError(msg='角色已存在') count = await role_dao.update(db, pk, obj) + # TODO: 重构缓存清理 for user in await role.awaitable_attrs.users: await redis_client.delete_prefix(f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}') return count @@ -151,6 +150,7 @@ async def update_role_menu(*, db: AsyncSession, pk: int, menu_ids: UpdateRoleMen if not menu: raise errors.NotFoundError(msg='菜单不存在') count = await role_dao.update_menus(db, pk, menu_ids) + # TODO: 重构缓存清理 for user in await role.awaitable_attrs.users: await redis_client.delete_prefix(f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}') return count @@ -174,6 +174,7 @@ async def update_role_scope(*, db: AsyncSession, pk: int, scope_ids: UpdateRoleS if not scope: raise errors.NotFoundError(msg='数据范围不存在') count = await role_dao.update_scopes(db, pk, scope_ids) + # TODO: 重构缓存清理 for user in await role.awaitable_attrs.users: await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}') return count @@ -189,6 +190,7 @@ async def delete(*, db: AsyncSession, obj: DeleteRoleParam) -> int: """ count = await role_dao.delete(db, obj.pks) + # TODO: 重构缓存清理 for pk in obj.pks: role = await role_dao.get(db, pk) if role: From ce9a4d4f8c66d84d5de3745bc66f4d6fc74d566f Mon Sep 17 00:00:00 2001 From: Wu Clan Date: Sun, 9 Nov 2025 23:27:31 +0800 Subject: [PATCH 18/25] Update the data scope and rule crud --- backend/app/admin/crud/crud_data_rule.py | 2 +- backend/app/admin/crud/crud_data_scope.py | 47 +++++++++++++++++------ backend/app/admin/schema/data_scope.py | 7 ++++ 3 files changed, 43 insertions(+), 13 deletions(-) diff --git a/backend/app/admin/crud/crud_data_rule.py b/backend/app/admin/crud/crud_data_rule.py index 48dc6d16..635c8e34 100644 --- a/backend/app/admin/crud/crud_data_rule.py +++ b/backend/app/admin/crud/crud_data_rule.py @@ -33,7 +33,7 @@ async def get_select(self, name: str | None) -> Select: if name is not None: filters['name__like'] = f'%{name}%' - return await self.select_order('id', load_strategies={'scopes': 'noload'}, **filters) + return await self.select_order('id', **filters) async def get_by_name(self, db: AsyncSession, name: str) -> DataRule | None: """ diff --git a/backend/app/admin/crud/crud_data_scope.py b/backend/app/admin/crud/crud_data_scope.py index 2383db82..068a7d0a 100644 --- a/backend/app/admin/crud/crud_data_scope.py +++ b/backend/app/admin/crud/crud_data_scope.py @@ -1,11 +1,19 @@ from collections.abc import Sequence +from typing import Any -from sqlalchemy import Select, select +from sqlalchemy import Select, delete, insert from sqlalchemy.ext.asyncio import AsyncSession -from sqlalchemy_crud_plus import CRUDPlus +from sqlalchemy_crud_plus import CRUDPlus, JoinConfig from backend.app.admin.model import DataRule, DataScope -from backend.app.admin.schema.data_scope import CreateDataScopeParam, UpdateDataScopeParam, UpdateDataScopeRuleParam +from backend.app.admin.model.m2m import data_scope_rule +from backend.app.admin.schema.data_scope import ( + CreateDataScopeParam, + CreateDataScopeRuleParam, + UpdateDataScopeParam, + UpdateDataScopeRuleParam, +) +from backend.utils.serializers import select_join_serialize class CRUDDataScope(CRUDPlus[DataScope]): @@ -31,7 +39,7 @@ async def get_by_name(self, db: AsyncSession, name: str) -> DataScope | None: """ return await self.select_model_by_column(db, name=name) - async def get_with_relation(self, db: AsyncSession, pk: int) -> DataScope: + async def get_with_relation(self, db: AsyncSession, pk: int) -> Any: """ 获取数据范围关联数据 @@ -39,7 +47,16 @@ async def get_with_relation(self, db: AsyncSession, pk: int) -> DataScope: :param pk: 范围 ID :return: """ - return await self.select_model(db, pk, load_strategies=['rules']) + result = await self.select_models( + db, + id=pk, + join_conditions=[ + JoinConfig(model=data_scope_rule, join_on=data_scope_rule.c.data_scope_id == self.model.id), + JoinConfig(model=DataRule, join_on=DataRule.id == data_scope_rule.c.data_rule_id, fill_result=True), + ], + ) + + return await select_join_serialize(result, relationships=['DataScope-m2m-DataRule']) async def get_all(self, db: AsyncSession) -> Sequence[DataScope]: """ @@ -65,7 +82,7 @@ async def get_select(self, name: str | None, status: int | None) -> Select: if status is not None: filters['status'] = status - return await self.select_order('id', load_strategies={'rules': 'noload', 'roles': 'noload'}, **filters) + return await self.select_order('id', **filters) async def create(self, db: AsyncSession, obj: CreateDataScopeParam) -> None: """ @@ -88,7 +105,8 @@ async def update(self, db: AsyncSession, pk: int, obj: UpdateDataScopeParam) -> """ return await self.update_model(db, pk, obj) - async def update_rules(self, db: AsyncSession, pk: int, rule_ids: UpdateDataScopeRuleParam) -> int: + @staticmethod + async def update_rules(db: AsyncSession, pk: int, rule_ids: UpdateDataScopeRuleParam) -> int: """ 更新数据范围规则 @@ -97,11 +115,16 @@ async def update_rules(self, db: AsyncSession, pk: int, rule_ids: UpdateDataScop :param rule_ids: 数据规则 ID 列表 :return: """ - current_data_scope = await self.get_with_relation(db, pk) - stmt = select(DataRule).where(DataRule.id.in_(rule_ids.rules)) - rules = await db.execute(stmt) - current_data_scope.rules = rules.scalars().all() - return len(current_data_scope.rules) + data_scope_rule_stmt = delete(data_scope_rule).where(data_scope_rule.c.data_scope_id == pk) + await db.execute(data_scope_rule_stmt) + + data_scope_rule_data = [ + CreateDataScopeRuleParam(data_scope_id=pk, data_rule_id=rule_id).model_dump() for rule_id in rule_ids.rules + ] + data_scope_rule_stmt = insert(data_scope_rule) + await db.execute(data_scope_rule_stmt, data_scope_rule_data) + + return len(rule_ids.rules) async def delete(self, db: AsyncSession, pks: list[int]) -> int: """ diff --git a/backend/app/admin/schema/data_scope.py b/backend/app/admin/schema/data_scope.py index b2444a4e..cee901c7 100644 --- a/backend/app/admin/schema/data_scope.py +++ b/backend/app/admin/schema/data_scope.py @@ -22,6 +22,13 @@ class UpdateDataScopeParam(DataScopeBase): """更新数据范围参数""" +class CreateDataScopeRuleParam(SchemaBase): + """创建数据范围规则参数""" + + data_scope_id: int = Field(description='数据范围 ID') + data_rule_id: int = Field(description='数据规则 ID') + + class UpdateDataScopeRuleParam(SchemaBase): """更新数据范围规则参数""" From 36f137c1aa73be1a745ca673c72038f27962a8e3 Mon Sep 17 00:00:00 2001 From: Wu Clan Date: Sun, 9 Nov 2025 23:40:45 +0800 Subject: [PATCH 19/25] Restore get_paginated to get_select --- backend/app/admin/crud/crud_role.py | 13 +++++-------- backend/app/admin/crud/crud_user.py | 16 ++++------------ backend/app/admin/service/role_service.py | 4 +++- backend/app/admin/service/user_service.py | 7 ++++++- backend/plugin/dict/crud/crud_dict_type.py | 11 ++++------- backend/plugin/dict/service/dict_type_service.py | 4 +++- 6 files changed, 25 insertions(+), 30 deletions(-) diff --git a/backend/app/admin/crud/crud_role.py b/backend/app/admin/crud/crud_role.py index bb86a46d..17d3ed96 100644 --- a/backend/app/admin/crud/crud_role.py +++ b/backend/app/admin/crud/crud_role.py @@ -1,7 +1,7 @@ from collections.abc import Sequence from typing import Any -from sqlalchemy import delete, insert +from sqlalchemy import Select, delete, insert from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy_crud_plus import CRUDPlus, JoinConfig @@ -15,7 +15,6 @@ UpdateRoleParam, UpdateRoleScopeParam, ) -from backend.common.pagination import paging_data from backend.utils.serializers import select_join_serialize @@ -62,11 +61,10 @@ async def get_all(self, db: AsyncSession) -> Sequence[Role]: """ return await self.select_models(db) - async def get_paginated(self, db: AsyncSession, name: str | None, status: int | None) -> dict[str, Any]: + async def get_select(self, name: str | None, status: int | None) -> Select: """ - 获取角色分页 + 获取角色列表查询表达式 - :param db: 数据库会话 :param name: 角色名称 :param status: 角色状态 :return: @@ -79,11 +77,10 @@ async def get_paginated(self, db: AsyncSession, name: str | None, status: int | if status is not None: filters['status'] = status - role_select = await self.select_order( + return await self.select_order( 'id', - **filters, + **filters ) - return await paging_data(db, role_select) async def get_by_name(self, db: AsyncSession, name: str) -> Role | None: """ diff --git a/backend/app/admin/crud/crud_user.py b/backend/app/admin/crud/crud_user.py index 19149eb5..920e366c 100644 --- a/backend/app/admin/crud/crud_user.py +++ b/backend/app/admin/crud/crud_user.py @@ -2,7 +2,7 @@ import bcrypt -from sqlalchemy import delete, insert, select +from sqlalchemy import Select, delete, insert, select from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy_crud_plus import CRUDPlus, JoinConfig @@ -14,7 +14,6 @@ AddUserRoleParam, UpdateUserParam, ) -from backend.common.pagination import paging_data from backend.common.security.jwt import get_hash_password from backend.plugin.oauth2.crud.crud_user_social import user_social_dao from backend.utils.serializers import select_join_serialize @@ -205,13 +204,10 @@ async def reset_password(self, db: AsyncSession, pk: int, password: str) -> int: new_pwd = get_hash_password(password, salt) return await self.update_model(db, pk, {'password': new_pwd, 'salt': salt}) - async def get_paginated( - self, db: AsyncSession, dept: int | None, username: str | None, phone: str | None, status: int | None - ) -> dict[str, Any]: + async def get_select(self, dept: int | None, username: str | None, phone: str | None, status: int | None) -> Select: """ - 获取用户分页 + 获取用户列表查询表达式 - :param db: 数据库会话 :param dept: 部门 ID :param username: 用户名 :param phone: 电话号码 @@ -229,7 +225,7 @@ async def get_paginated( if status is not None: filters['status'] = status - user_select = await self.select_order( + return await self.select_order( 'id', 'desc', join_conditions=[ @@ -240,10 +236,6 @@ async def get_paginated( **filters, ) - data = await paging_data(db, user_select) - data['items'] = select_join_serialize(data['items'], relationships=['User-m2o-Dept', 'User-m2m-Role']) - return data - async def set_super(self, db: AsyncSession, user_id: int, *, is_super: bool) -> int: """ 设置用户超级管理员状态 diff --git a/backend/app/admin/service/role_service.py b/backend/app/admin/service/role_service.py index 268f0c17..bb1f34c6 100644 --- a/backend/app/admin/service/role_service.py +++ b/backend/app/admin/service/role_service.py @@ -15,6 +15,7 @@ UpdateRoleScopeParam, ) from backend.common.exception import errors +from backend.common.pagination import paging_data from backend.core.conf import settings from backend.database.redis import redis_client from backend.utils.build_tree import get_tree_data @@ -60,7 +61,8 @@ async def get_list(*, db: AsyncSession, name: str | None, status: int | None) -> :param status: 状态 :return: """ - return await role_dao.get_paginated(db=db, name=name, status=status) + role_select = await role_dao.get_select(name=name, status=status) + return await paging_data(db, role_select) @staticmethod async def get_menu_tree(*, db: AsyncSession, pk: int) -> list[dict[str, Any] | None]: diff --git a/backend/app/admin/service/user_service.py b/backend/app/admin/service/user_service.py index 380e0031..53b4c9f6 100644 --- a/backend/app/admin/service/user_service.py +++ b/backend/app/admin/service/user_service.py @@ -18,10 +18,12 @@ from backend.common.context import ctx from backend.common.enums import UserPermissionType from backend.common.exception import errors +from backend.common.pagination import paging_data from backend.common.response.response_code import CustomErrorCode from backend.common.security.jwt import get_token, jwt_decode, password_verify from backend.core.conf import settings from backend.database.redis import redis_client +from backend.utils.serializers import select_join_serialize class UserService: @@ -68,7 +70,10 @@ async def get_list(*, db: AsyncSession, dept: int, username: str, phone: str, st :param status: 状态 :return: """ - return await user_dao.get_paginated(db=db, dept=dept, username=username, phone=phone, status=status) + user_select = await user_dao.get_select(dept=dept, username=username, phone=phone, status=status) + data = await paging_data(db, user_select) + data['items'] = select_join_serialize(data['items'], relationships=['User-m2o-Dept', 'User-m2m-Role']) + return data @staticmethod async def create(*, db: AsyncSession, obj: AddUserParam) -> None: diff --git a/backend/plugin/dict/crud/crud_dict_type.py b/backend/plugin/dict/crud/crud_dict_type.py index bbed2dda..295c79e2 100644 --- a/backend/plugin/dict/crud/crud_dict_type.py +++ b/backend/plugin/dict/crud/crud_dict_type.py @@ -1,10 +1,9 @@ from collections.abc import Sequence -from typing import Any +from sqlalchemy import Select from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy_crud_plus import CRUDPlus -from backend.common.pagination import paging_data from backend.plugin.dict.crud.crud_dict_data import dict_data_dao from backend.plugin.dict.model import DictType from backend.plugin.dict.schema.dict_type import CreateDictTypeParam, UpdateDictTypeParam @@ -32,11 +31,10 @@ async def get_all(self, db: AsyncSession) -> Sequence[DictType]: """ return await self.select_models(db) - async def get_paginated(self, db: AsyncSession, name: str | None, code: str | None) -> dict[str, Any]: + async def get_select(self, name: str | None, code: str | None) -> Select: """ - 获取用户分页 + 获取字典类型列表查询表达式 - :param db: 数据库会话 :param name: 字典类型名称 :param code: 字典类型编码 :return: @@ -48,8 +46,7 @@ async def get_paginated(self, db: AsyncSession, name: str | None, code: str | No if code is not None: filters['code__like'] = f'%{code}%' - dict_type_select = await self.select_order('id', 'desc', **filters) - return await paging_data(db, dict_type_select) + return await self.select_order('id', 'desc', **filters) async def get_by_code(self, db: AsyncSession, code: str) -> DictType | None: """ diff --git a/backend/plugin/dict/service/dict_type_service.py b/backend/plugin/dict/service/dict_type_service.py index 1e840dc8..584e3e2a 100644 --- a/backend/plugin/dict/service/dict_type_service.py +++ b/backend/plugin/dict/service/dict_type_service.py @@ -4,6 +4,7 @@ from sqlalchemy.ext.asyncio import AsyncSession from backend.common.exception import errors +from backend.common.pagination import paging_data from backend.plugin.dict.crud.crud_dict_type import dict_type_dao from backend.plugin.dict.model import DictType from backend.plugin.dict.schema.dict_type import CreateDictTypeParam, DeleteDictTypeParam, UpdateDictTypeParam @@ -48,7 +49,8 @@ async def get_list(*, db: AsyncSession, name: str | None, code: str | None) -> d :param code: 字典类型编码 :return: """ - return await dict_type_dao.get_paginated(db=db, name=name, code=code) + dict_type_select = await dict_type_dao.get_select(name=name, code=code) + return await paging_data(db, dict_type_select) @staticmethod async def create(*, db: AsyncSession, obj: CreateDictTypeParam) -> None: From 98baaec89559bce500b8b19e95dc2d0fd711264f Mon Sep 17 00:00:00 2001 From: Wu Clan Date: Sun, 9 Nov 2025 23:42:29 +0800 Subject: [PATCH 20/25] Update the code generator plugin version --- backend/plugin/code_generator/plugin.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/plugin/code_generator/plugin.toml b/backend/plugin/code_generator/plugin.toml index a32c0d8d..94f6954c 100644 --- a/backend/plugin/code_generator/plugin.toml +++ b/backend/plugin/code_generator/plugin.toml @@ -1,6 +1,6 @@ [plugin] summary = '代码生成' -version = '0.0.5' +version = '0.0.6' description = '生成通用业务代码' author = 'wu-clan' From da8d698d35101d4f43f1eb64990384c82675a40a Mon Sep 17 00:00:00 2001 From: Wu Clan Date: Sun, 9 Nov 2025 23:57:14 +0800 Subject: [PATCH 21/25] Add the py version in pre-commit --- .pre-commit-config.yaml | 3 +++ backend/app/admin/crud/crud_role.py | 5 +---- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8501d2dd..e064c2ef 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,3 +1,6 @@ +default_language_version: + python: '>= 3.10' + repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v6.0.0 diff --git a/backend/app/admin/crud/crud_role.py b/backend/app/admin/crud/crud_role.py index 17d3ed96..007debc6 100644 --- a/backend/app/admin/crud/crud_role.py +++ b/backend/app/admin/crud/crud_role.py @@ -77,10 +77,7 @@ async def get_select(self, name: str | None, status: int | None) -> Select: if status is not None: filters['status'] = status - return await self.select_order( - 'id', - **filters - ) + return await self.select_order('id', **filters) async def get_by_name(self, db: AsyncSession, name: str) -> Role | None: """ From 022112161ab8f3931155806630d87039d278e3f6 Mon Sep 17 00:00:00 2001 From: Wu Clan Date: Mon, 10 Nov 2025 14:49:49 +0800 Subject: [PATCH 22/25] Remove the plugin include parameter config --- backend/plugin/tools.py | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/backend/plugin/tools.py b/backend/plugin/tools.py index 6b1b2bd1..beed473e 100644 --- a/backend/plugin/tools.py +++ b/backend/plugin/tools.py @@ -142,14 +142,7 @@ def parse_plugin_config() -> tuple[list[dict[str, Any]], list[dict[str, Any]]]: raise PluginConfigError(f'插件 {plugin} 配置文件缺少必要字段: {", ".join(missing_fields)}') if data.get('api'): - # TODO: 删除过时的 include 配置 - include = data.get('app', {}).get('include') - if include: - warnings.warn( - f'插件 {plugin} 配置 app.include 即将在未来版本中弃用,请尽快更新配置为 app.extend, 详情:https://fastapi-practices.github.io/fastapi_best_architecture_docs/plugin/dev.html#%E6%8F%92%E4%BB%B6%E9%85%8D%E7%BD%AE', - FutureWarning, - ) - if not include and not data.get('app', {}).get('extend'): + if not data.get('app', {}).get('extend'): raise PluginConfigError(f'扩展级插件 {plugin} 配置文件缺少 app.extend 配置') extend_plugins.append(data) else: @@ -219,8 +212,7 @@ def inject_extend_router(plugin: dict[str, Any]) -> None: # 获取目标 app 路由 relative_path = os.path.relpath(root, plugin_api_path) - # TODO: 删除过时的 include 配置 - app_name = plugin.get('app', {}).get('include') or plugin.get('app', {}).get('extend') + app_name = plugin.get('app', {}).get('extend') target_module_path = f'backend.app.{app_name}.api.{relative_path.replace(os.sep, ".")}' target_module = import_module_cached(target_module_path) target_router = getattr(target_module, 'router', None) From 07fb3710d40e4992819661fd280b28252e03803d Mon Sep 17 00:00:00 2001 From: Wu Clan Date: Mon, 10 Nov 2025 15:48:16 +0800 Subject: [PATCH 23/25] Add more cache cleaning TODO --- backend/app/admin/model/data_rule.py | 2 +- backend/app/admin/service/data_rule_service.py | 2 ++ backend/app/admin/service/data_scope_service.py | 1 + backend/common/security/permission.py | 4 ++-- backend/core/conf.py | 2 +- 5 files changed, 7 insertions(+), 4 deletions(-) diff --git a/backend/app/admin/model/data_rule.py b/backend/app/admin/model/data_rule.py index e3db5638..44b57091 100644 --- a/backend/app/admin/model/data_rule.py +++ b/backend/app/admin/model/data_rule.py @@ -12,7 +12,7 @@ class DataRule(Base): id: Mapped[id_key] = mapped_column(init=False) name: Mapped[str] = mapped_column(sa.String(512), unique=True, comment='名称') - model: Mapped[str] = mapped_column(sa.String(64), comment='SQLA 模型名,对应 DATA_PERMISSION_MODELS 键名') + model: Mapped[str] = mapped_column(sa.String(64), comment='模型名称') column: Mapped[str] = mapped_column(sa.String(32), comment='模型字段名') operator: Mapped[int] = mapped_column(comment='运算符(0:and、1:or)') expression: Mapped[int] = mapped_column( diff --git a/backend/app/admin/service/data_rule_service.py b/backend/app/admin/service/data_rule_service.py index 0c6aebc1..2b7089e8 100644 --- a/backend/app/admin/service/data_rule_service.py +++ b/backend/app/admin/service/data_rule_service.py @@ -113,6 +113,7 @@ async def update(*, db: AsyncSession, pk: int, obj: UpdateDataRuleParam) -> int: if data_rule.name != obj.name and await data_rule_dao.get_by_name(db, obj.name): raise errors.ConflictError(msg='数据规则已存在') count = await data_rule_dao.update(db, pk, obj) + # TODO: 重构缓存清理 return count @staticmethod @@ -125,6 +126,7 @@ async def delete(*, db: AsyncSession, obj: DeleteDataRuleParam) -> int: :return: """ count = await data_rule_dao.delete(db, obj.pks) + # TODO: 重构缓存清理 return count diff --git a/backend/app/admin/service/data_scope_service.py b/backend/app/admin/service/data_scope_service.py index 9ba0bc47..5aacff0e 100644 --- a/backend/app/admin/service/data_scope_service.py +++ b/backend/app/admin/service/data_scope_service.py @@ -121,6 +121,7 @@ async def update_data_scope_rule(*, db: AsyncSession, pk: int, rule_ids: UpdateD :return: """ count = await data_scope_dao.update_rules(db, pk, rule_ids) + # TODO: 重构缓存清理 return count @staticmethod diff --git a/backend/common/security/permission.py b/backend/common/security/permission.py index 59f520a1..75c948c2 100644 --- a/backend/common/security/permission.py +++ b/backend/common/security/permission.py @@ -77,7 +77,7 @@ def filter_data_permission(request_user: GetUserInfoWithRelationDetail) -> Colum # 验证规则模型 rule_model = data_rule.model if rule_model not in settings.DATA_PERMISSION_MODELS: - raise errors.NotFoundError(msg='数据规则模型不存在') + raise errors.NotFoundError(msg='数据规则可用模型不存在') model_ins = dynamic_import_data_model(settings.DATA_PERMISSION_MODELS[rule_model]) # 验证规则列 @@ -86,7 +86,7 @@ def filter_data_permission(request_user: GetUserInfoWithRelationDetail) -> Colum ] column = data_rule.column if column not in model_columns: - raise errors.NotFoundError(msg='数据规则模型列不存在') + raise errors.NotFoundError(msg='数据规则可用模型列不存在') # 构建过滤条件 column_obj = getattr(model_ins, column) diff --git a/backend/core/conf.py b/backend/core/conf.py index 29194fa8..fecd35c1 100644 --- a/backend/core/conf.py +++ b/backend/core/conf.py @@ -90,7 +90,7 @@ class Settings(BaseSettings): # 数据权限 DATA_PERMISSION_MODELS: dict[str, str] = { # 允许进行数据过滤的 SQLA 模型,它必须以模块字符串的方式定义 - '部门': 'backend.app.admin.model.Dept', + 'Dept': 'backend.app.admin.model.Dept', } DATA_PERMISSION_COLUMN_EXCLUDE: list[str] = [ # 排除允许进行数据过滤的 SQLA 模型列 'id', From aaff9e034f4854e9dfc64994f2ee0ceb22e17862 Mon Sep 17 00:00:00 2001 From: Wu Clan Date: Mon, 10 Nov 2025 15:52:46 +0800 Subject: [PATCH 24/25] Rename get_with_relation to get_join --- backend/app/admin/crud/crud_data_scope.py | 4 ++-- backend/app/admin/crud/crud_role.py | 2 +- backend/app/admin/service/data_scope_service.py | 2 +- backend/app/admin/service/role_service.py | 6 +++--- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/backend/app/admin/crud/crud_data_scope.py b/backend/app/admin/crud/crud_data_scope.py index 068a7d0a..cf8fa8f6 100644 --- a/backend/app/admin/crud/crud_data_scope.py +++ b/backend/app/admin/crud/crud_data_scope.py @@ -39,7 +39,7 @@ async def get_by_name(self, db: AsyncSession, name: str) -> DataScope | None: """ return await self.select_model_by_column(db, name=name) - async def get_with_relation(self, db: AsyncSession, pk: int) -> Any: + async def get_join(self, db: AsyncSession, pk: int) -> Any: """ 获取数据范围关联数据 @@ -56,7 +56,7 @@ async def get_with_relation(self, db: AsyncSession, pk: int) -> Any: ], ) - return await select_join_serialize(result, relationships=['DataScope-m2m-DataRule']) + return select_join_serialize(result, relationships=['DataScope-m2m-DataRule']) async def get_all(self, db: AsyncSession) -> Sequence[DataScope]: """ diff --git a/backend/app/admin/crud/crud_role.py b/backend/app/admin/crud/crud_role.py index 007debc6..65829e62 100644 --- a/backend/app/admin/crud/crud_role.py +++ b/backend/app/admin/crud/crud_role.py @@ -31,7 +31,7 @@ async def get(self, db: AsyncSession, role_id: int) -> Role | None: """ return await self.select_model(db, role_id) - async def get_with_relation(self, db: AsyncSession, role_id: int) -> Any: + async def get_join(self, db: AsyncSession, role_id: int) -> Any: """ 获取角色及关联数据 diff --git a/backend/app/admin/service/data_scope_service.py b/backend/app/admin/service/data_scope_service.py index 5aacff0e..a8f87089 100644 --- a/backend/app/admin/service/data_scope_service.py +++ b/backend/app/admin/service/data_scope_service.py @@ -57,7 +57,7 @@ async def get_rules(*, db: AsyncSession, pk: int) -> DataScope: :return: """ - data_scope = await data_scope_dao.get_with_relation(db, pk) + data_scope = await data_scope_dao.get_join(db, pk) if not data_scope: raise errors.NotFoundError(msg='数据范围不存在') return data_scope diff --git a/backend/app/admin/service/role_service.py b/backend/app/admin/service/role_service.py index bb1f34c6..b1594219 100644 --- a/backend/app/admin/service/role_service.py +++ b/backend/app/admin/service/role_service.py @@ -34,7 +34,7 @@ async def get(*, db: AsyncSession, pk: int) -> Role: :return: """ - role = await role_dao.get_with_relation(db, pk) + role = await role_dao.get_join(db, pk) if not role: raise errors.NotFoundError(msg='角色不存在') return role @@ -74,7 +74,7 @@ async def get_menu_tree(*, db: AsyncSession, pk: int) -> list[dict[str, Any] | N :return: """ - role = await role_dao.get_with_relation(db, pk) + role = await role_dao.get_join(db, pk) if not role: raise errors.NotFoundError(msg='角色不存在') menu_tree = get_tree_data(role.menus) if role.menus else [] @@ -90,7 +90,7 @@ async def get_scopes(*, db: AsyncSession, pk: int) -> list[int]: :return: """ - role = await role_dao.get_with_relation(db, pk) + role = await role_dao.get_join(db, pk) if not role: raise errors.NotFoundError(msg='角色不存在') scope_ids = [scope.id for scope in role.scopes] From b1379d378a49d9d30f5623134e9142cc90443641 Mon Sep 17 00:00:00 2001 From: Wu Clan Date: Mon, 10 Nov 2025 21:00:50 +0800 Subject: [PATCH 25/25] Add the user cache clear --- .../app/admin/service/data_rule_service.py | 5 +- .../app/admin/service/data_scope_service.py | 19 +--- backend/app/admin/service/menu_service.py | 16 +-- backend/app/admin/service/role_service.py | 26 ++--- backend/app/admin/utils/__init__.py | 1 + backend/app/admin/utils/cache.py | 98 +++++++++++++++++++ 6 files changed, 120 insertions(+), 45 deletions(-) create mode 100644 backend/app/admin/utils/__init__.py create mode 100644 backend/app/admin/utils/cache.py diff --git a/backend/app/admin/service/data_rule_service.py b/backend/app/admin/service/data_rule_service.py index 2b7089e8..4980a48a 100644 --- a/backend/app/admin/service/data_rule_service.py +++ b/backend/app/admin/service/data_rule_service.py @@ -11,6 +11,7 @@ GetDataRuleColumnDetail, UpdateDataRuleParam, ) +from backend.app.admin.utils.cache import user_cache_manager from backend.common.exception import errors from backend.common.pagination import paging_data from backend.core.conf import settings @@ -113,7 +114,7 @@ async def update(*, db: AsyncSession, pk: int, obj: UpdateDataRuleParam) -> int: if data_rule.name != obj.name and await data_rule_dao.get_by_name(db, obj.name): raise errors.ConflictError(msg='数据规则已存在') count = await data_rule_dao.update(db, pk, obj) - # TODO: 重构缓存清理 + await user_cache_manager.clear_by_data_rule_id(db, [pk]) return count @staticmethod @@ -126,7 +127,7 @@ async def delete(*, db: AsyncSession, obj: DeleteDataRuleParam) -> int: :return: """ count = await data_rule_dao.delete(db, obj.pks) - # TODO: 重构缓存清理 + await user_cache_manager.clear_by_data_rule_id(db, obj.pks) return count diff --git a/backend/app/admin/service/data_scope_service.py b/backend/app/admin/service/data_scope_service.py index a8f87089..5ed739ff 100644 --- a/backend/app/admin/service/data_scope_service.py +++ b/backend/app/admin/service/data_scope_service.py @@ -11,10 +11,9 @@ UpdateDataScopeParam, UpdateDataScopeRuleParam, ) +from backend.app.admin.utils.cache import user_cache_manager from backend.common.exception import errors from backend.common.pagination import paging_data -from backend.core.conf import settings -from backend.database.redis import redis_client class DataScopeService: @@ -105,10 +104,7 @@ async def update(*, db: AsyncSession, pk: int, obj: UpdateDataScopeParam) -> int if data_scope.name != obj.name and await data_scope_dao.get_by_name(db, obj.name): raise errors.ConflictError(msg='数据范围已存在') count = await data_scope_dao.update(db, pk, obj) - # TODO: 重构缓存清理 - for role in await data_scope.awaitable_attrs.roles: - for user in await role.awaitable_attrs.users: - await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}') + await user_cache_manager.clear_by_data_scope_id(db, [pk]) return count @staticmethod @@ -116,12 +112,13 @@ async def update_data_scope_rule(*, db: AsyncSession, pk: int, rule_ids: UpdateD """ 更新数据范围规则 + :param db: 数据库会话 :param pk: 范围 ID :param rule_ids: 规则 ID 列表 :return: """ count = await data_scope_dao.update_rules(db, pk, rule_ids) - # TODO: 重构缓存清理 + await user_cache_manager.clear_by_data_scope_id(db, [pk]) return count @staticmethod @@ -134,13 +131,7 @@ async def delete(*, db: AsyncSession, obj: DeleteDataScopeParam) -> int: :return: """ count = await data_scope_dao.delete(db, obj.pks) - # TODO: 重构缓存清理 - for pk in obj.pks: - data_rule = await data_scope_dao.get(db, pk) - if data_rule: - for role in await data_rule.awaitable_attrs.roles: - for user in await role.awaitable_attrs.users: - await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}') + await user_cache_manager.clear_by_data_scope_id(db, obj.pks) return count diff --git a/backend/app/admin/service/menu_service.py b/backend/app/admin/service/menu_service.py index 6296ba3d..3be7c553 100644 --- a/backend/app/admin/service/menu_service.py +++ b/backend/app/admin/service/menu_service.py @@ -6,9 +6,8 @@ from backend.app.admin.crud.crud_menu import menu_dao from backend.app.admin.model import Menu from backend.app.admin.schema.menu import CreateMenuParam, UpdateMenuParam +from backend.app.admin.utils.cache import user_cache_manager from backend.common.exception import errors -from backend.core.conf import settings -from backend.database.redis import redis_client from backend.utils.build_tree import get_tree_data, get_vben5_tree_data @@ -112,10 +111,7 @@ async def update(*, db: AsyncSession, pk: int, obj: UpdateMenuParam) -> int: if obj.parent_id == menu.id: raise errors.ForbiddenError(msg='禁止关联自身为父级') count = await menu_dao.update(db, pk, obj) - # TODO: 重构缓存清理 - for role in await menu.awaitable_attrs.roles: - for user in await role.awaitable_attrs.users: - await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}') + await user_cache_manager.clear_by_menu_id(db, [pk]) return count @staticmethod @@ -131,13 +127,9 @@ async def delete(*, db: AsyncSession, pk: int) -> int: children = await menu_dao.get_children(db, pk) if children: raise errors.ConflictError(msg='菜单下存在子菜单,无法删除') - menu = await menu_dao.get(db, pk) count = await menu_dao.delete(db, pk) - # TODO: 重构缓存清理 - if menu: - for role in await menu.awaitable_attrs.roles: - for user in await role.awaitable_attrs.users: - await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}') + if count: + await user_cache_manager.clear_by_menu_id(db, [pk]) return count diff --git a/backend/app/admin/service/role_service.py b/backend/app/admin/service/role_service.py index b1594219..3dbe0adc 100644 --- a/backend/app/admin/service/role_service.py +++ b/backend/app/admin/service/role_service.py @@ -14,10 +14,9 @@ UpdateRoleParam, UpdateRoleScopeParam, ) +from backend.app.admin.utils.cache import user_cache_manager from backend.common.exception import errors from backend.common.pagination import paging_data -from backend.core.conf import settings -from backend.database.redis import redis_client from backend.utils.build_tree import get_tree_data @@ -128,9 +127,8 @@ async def update(*, db: AsyncSession, pk: int, obj: UpdateRoleParam) -> int: if role.name != obj.name and await role_dao.get_by_name(db, obj.name): raise errors.ConflictError(msg='角色已存在') count = await role_dao.update(db, pk, obj) - # TODO: 重构缓存清理 - for user in await role.awaitable_attrs.users: - await redis_client.delete_prefix(f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}') + # 清理该角色所有用户的缓存 + await user_cache_manager.clear_by_role_id(db, [pk]) return count @staticmethod @@ -152,9 +150,8 @@ async def update_role_menu(*, db: AsyncSession, pk: int, menu_ids: UpdateRoleMen if not menu: raise errors.NotFoundError(msg='菜单不存在') count = await role_dao.update_menus(db, pk, menu_ids) - # TODO: 重构缓存清理 - for user in await role.awaitable_attrs.users: - await redis_client.delete_prefix(f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}') + # 清理该角色所有用户的缓存 + await user_cache_manager.clear_by_role_id(db, [pk]) return count @staticmethod @@ -176,9 +173,8 @@ async def update_role_scope(*, db: AsyncSession, pk: int, scope_ids: UpdateRoleS if not scope: raise errors.NotFoundError(msg='数据范围不存在') count = await role_dao.update_scopes(db, pk, scope_ids) - # TODO: 重构缓存清理 - for user in await role.awaitable_attrs.users: - await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}') + # 清理该角色所有用户的缓存 + await user_cache_manager.clear_by_role_id(db, [pk]) return count @staticmethod @@ -192,12 +188,8 @@ async def delete(*, db: AsyncSession, obj: DeleteRoleParam) -> int: """ count = await role_dao.delete(db, obj.pks) - # TODO: 重构缓存清理 - for pk in obj.pks: - role = await role_dao.get(db, pk) - if role: - for user in await role.awaitable_attrs.users: - await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}') + # 清理这些角色所有用户的缓存 + await user_cache_manager.clear_by_role_id(db, obj.pks) return count diff --git a/backend/app/admin/utils/__init__.py b/backend/app/admin/utils/__init__.py new file mode 100644 index 00000000..e5a0d9b4 --- /dev/null +++ b/backend/app/admin/utils/__init__.py @@ -0,0 +1 @@ +#!/usr/bin/env python3 diff --git a/backend/app/admin/utils/cache.py b/backend/app/admin/utils/cache.py new file mode 100644 index 00000000..ffd3f0d0 --- /dev/null +++ b/backend/app/admin/utils/cache.py @@ -0,0 +1,98 @@ +from collections.abc import Sequence + +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from backend.app.admin.model.m2m import data_scope_rule, role_data_scope, role_menu, user_role +from backend.core.conf import settings +from backend.database.redis import redis_client + + +class UserCacheManager: + """用户缓存管理""" + + @staticmethod + async def clear(user_ids: Sequence[int]) -> None: + """ + 清理用户缓存 + + :param user_ids: 用户 ID 列表 + :return: + """ + for user_id in user_ids: + await redis_client.delete_prefix(f'{settings.JWT_USER_REDIS_PREFIX}:{user_id}') + + async def clear_by_role_id(self, db: AsyncSession, role_ids: list[int]) -> None: + """ + 通过角色 ID 清理用户缓存 + + :param db: 数据库会话 + :param role_ids: 角色 ID 列表 + :return: + """ + stmt = select(user_role.c.user_id).where(user_role.c.role_id.in_(role_ids)).distinct() + result = await db.execute(stmt) + user_ids = result.scalars().all() + + await self.clear(user_ids) + + async def clear_by_menu_id(self, db: AsyncSession, menu_ids: list[int]) -> None: + """ + 通过菜单 ID 清理用户缓存 + + :param db: 数据库会话 + :param menu_ids: 菜单 ID 列表 + :return: + """ + stmt = ( + select(user_role.c.user_id) + .join(role_menu, user_role.c.role_id == role_menu.c.role_id) + .where(role_menu.c.menu_id.in_(menu_ids)) + .distinct() + ) + result = await db.execute(stmt) + user_ids = result.scalars().all() + + await self.clear(user_ids) + + async def clear_by_data_scope_id(self, db: AsyncSession, scope_ids: list[int]) -> None: + """ + 通过数据范围 ID 清理用户缓存 + + :param db: 数据库会话 + :param scope_ids: 数据范围 ID 列表 + :return: + """ + stmt = ( + select(user_role.c.user_id) + .join(role_data_scope, user_role.c.role_id == role_data_scope.c.role_id) + .where(role_data_scope.c.data_scope_id.in_(scope_ids)) + .distinct() + ) + result = await db.execute(stmt) + user_ids = result.scalars().all() + + await self.clear(user_ids) + + async def clear_by_data_rule_id(self, db: AsyncSession, rule_ids: list[int]) -> None: + """ + 通过数据规则 ID 清理用户缓存 + + :param db: 数据库会话 + :param rule_ids: 数据规则 ID 列表 + :return: + """ + stmt = ( + select(user_role.c.user_id) + .join(role_data_scope, user_role.c.role_id == role_data_scope.c.role_id) + .join(data_scope_rule, role_data_scope.c.data_scope_id == data_scope_rule.c.data_scope_id) + .where(data_scope_rule.c.data_rule_id.in_(rule_ids)) + .distinct() + ) + result = await db.execute(stmt) + user_ids = result.scalars().all() + + await self.clear(user_ids) + + +user_cache_manager: UserCacheManager = UserCacheManager()