diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 5877287..b46fe77 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -5,7 +5,8 @@ "Bash(findstr:*)", "Bash(find:*)", "Bash(yarn tsc:*)", - "Bash(dir:*)" + "Bash(dir:*)", + "mcp__server-notify__notify" ] } } diff --git a/ee b/ee index 9dd69bd..cc32d8d 160000 --- a/ee +++ b/ee @@ -1 +1 @@ -Subproject commit 9dd69bd25760a3122fe10264d7e833aed81d860d +Subproject commit cc32d8db91d486042d4ea146e775e2f7e8ad753a diff --git a/main.py b/main.py index 25b24e0..6a42804 100644 --- a/main.py +++ b/main.py @@ -17,24 +17,26 @@ from utils.http.http_exceptions import raise_internal_error from utils.lifespan import lifespan # 尝试加载企业版功能 +_has_ee: bool = False try: from ee import init_ee from ee.license import LicenseError + from ee.routers import ee_router - async def _init_ee_and_routes() -> None: + _has_ee = True + + async def _init_ee() -> None: + """启动时验证许可证,路由由 license_valid_required 依赖保护""" try: await init_ee() except LicenseError as exc: l.critical(f"许可证验证失败: {exc}") raise SystemExit(1) from exc - from ee.routers import ee_router - from routers.api.v1 import router as v1_router - v1_router.include_router(ee_router) - - lifespan.add_startup(_init_ee_and_routes) -except ImportError: - l.info("以 Community 版本运行") + lifespan.add_startup(_init_ee) +except ImportError as exc: + ee_router = None + l.info(f"以 Community 版本运行 (原因: {exc})") STATICS_DIR: Path = (Path(__file__).parent / "statics").resolve() """前端静态文件目录(由 Docker 构建时复制)""" @@ -95,6 +97,8 @@ async def handle_unexpected_exceptions( # 挂载路由 app.include_router(router) +if _has_ee: + app.include_router(ee_router, prefix="/api/v1") # 挂载 WebDAV 协议端点(优先于 SPA catch-all) app.mount("/dav", dav_app) diff --git a/routers/api/v1/__init__.py b/routers/api/v1/__init__.py index f580ebc..466ebdf 100644 --- a/routers/api/v1/__init__.py +++ b/routers/api/v1/__init__.py @@ -5,6 +5,7 @@ from utils.conf import appmeta from .admin import admin_router from .callback import callback_router +from .category import category_router from .directory import directory_router from .download import download_router from .file import router as file_router @@ -14,7 +15,6 @@ from .trash import trash_router from .site import site_router from .slave import slave_router from .user import user_router -from .vas import vas_router from .webdav import webdav_router router = APIRouter(prefix="/v1") @@ -24,6 +24,7 @@ router = APIRouter(prefix="/v1") if appmeta.mode == "master": router.include_router(admin_router) router.include_router(callback_router) + router.include_router(category_router) router.include_router(directory_router) router.include_router(download_router) router.include_router(file_router) @@ -32,7 +33,6 @@ if appmeta.mode == "master": router.include_router(site_router) router.include_router(trash_router) router.include_router(user_router) - router.include_router(vas_router) router.include_router(webdav_router) elif appmeta.mode == "slave": router.include_router(slave_router) diff --git a/routers/api/v1/admin/__init__.py b/routers/api/v1/admin/__init__.py index e9fdd5d..52c4289 100644 --- a/routers/api/v1/admin/__init__.py +++ b/routers/api/v1/admin/__init__.py @@ -16,6 +16,12 @@ from sqlmodels.setting import ( from sqlmodels.setting import SettingsType from utils import http_exceptions from utils.conf import appmeta + +try: + from ee.service import get_cached_license +except ImportError: + get_cached_license = None + from .file import admin_file_router from .file_app import admin_file_app_router from .group import admin_group_router @@ -24,7 +30,6 @@ from .share import admin_share_router from .task import admin_task_router from .user import admin_user_router from .theme import admin_theme_router -from .vas import admin_vas_router class Aria2TestRequest(SQLModelBase): @@ -50,7 +55,6 @@ admin_router.include_router(admin_policy_router) admin_router.include_router(admin_share_router) admin_router.include_router(admin_task_router) admin_router.include_router(admin_theme_router) -admin_router.include_router(admin_vas_router) # 离线下载 /api/admin/aria2 admin_aria2_router = APIRouter( @@ -159,14 +163,24 @@ async def router_admin_get_summary(session: SessionDep) -> AdminSummaryResponse: if site_url_setting and site_url_setting.value: site_urls.append(site_url_setting.value) - # 许可证信息(从设置读取或使用默认值) - license_info = LicenseInfo( - expired_at=now + timedelta(days=365), - signed_at=now, - root_domains=[], - domains=[], - vol_domains=[], - ) + # 许可证信息(Pro 版本从缓存读取,CE 版本永不过期) + if appmeta.IsPro and get_cached_license: + payload = get_cached_license() + license_info = LicenseInfo( + expired_at=payload.expires_at, + signed_at=payload.issued_at, + root_domains=[], + domains=[payload.domain], + vol_domains=[], + ) + else: + license_info = LicenseInfo( + expired_at=datetime.max, + signed_at=now, + root_domains=[], + domains=[], + vol_domains=[], + ) # 版本信息 version_info = VersionInfo( @@ -225,11 +239,11 @@ async def router_admin_update_settings( if existing: existing.value = item.value - await existing.save(session) + existing = await existing.save(session) updated_count += 1 else: new_setting = Setting(type=item.type, name=item.name, value=item.value) - await new_setting.save(session) + new_setting = await new_setting.save(session) created_count += 1 l.info(f"管理员更新了 {updated_count} 个设置项,新建了 {created_count} 个设置项") diff --git a/routers/api/v1/admin/file/__init__.py b/routers/api/v1/admin/file/__init__.py index 3bb98a7..695f80f 100644 --- a/routers/api/v1/admin/file/__init__.py +++ b/routers/api/v1/admin/file/__init__.py @@ -54,7 +54,7 @@ async def _set_ban_recursive( obj.banned_by = None obj.ban_reason = None - await obj.save(session) + obj = await obj.save(session) count += 1 return count @@ -131,9 +131,7 @@ async def router_admin_preview_file( :param file_id: 文件UUID :return: 文件内容 """ - file_obj = await Object.get(session, Object.id == file_id) - if not file_obj: - raise HTTPException(status_code=404, detail="文件不存在") + file_obj = await Object.get_exist_one(session, file_id) if not file_obj.is_file: raise HTTPException(status_code=400, detail="对象不是文件") @@ -182,9 +180,7 @@ async def router_admin_ban_file( :param claims: 当前管理员 JWT claims :return: 封禁结果 """ - file_obj = await Object.get(session, Object.id == file_id) - if not file_obj: - raise HTTPException(status_code=404, detail="文件不存在") + file_obj = await Object.get_exist_one(session, file_id) count = await _set_ban_recursive(session, file_obj, request.ban, claims.sub, request.reason) @@ -212,9 +208,7 @@ async def router_admin_delete_file( :param delete_physical: 是否同时删除物理文件 :return: 删除结果 """ - file_obj = await Object.get(session, Object.id == file_id) - if not file_obj: - raise HTTPException(status_code=404, detail="文件不存在") + file_obj = await Object.get_exist_one(session, file_id) if not file_obj.is_file: raise HTTPException(status_code=400, detail="对象不是文件") diff --git a/routers/api/v1/admin/file_app/__init__.py b/routers/api/v1/admin/file_app/__init__.py index b82f115..f5ad68f 100644 --- a/routers/api/v1/admin/file_app/__init__.py +++ b/routers/api/v1/admin/file_app/__init__.py @@ -151,9 +151,7 @@ async def get_file_app( 错误处理: - 404: 应用不存在 """ - app: FileApp | None = await FileApp.get(session, FileApp.id == app_id) - if not app: - http_exceptions.raise_not_found("应用不存在") + app = await FileApp.get_exist_one(session, app_id) extensions = await FileAppExtension.get( session, @@ -186,9 +184,7 @@ async def update_file_app( - 404: 应用不存在 - 409: 新 app_key 已被其他应用使用 """ - app: FileApp | None = await FileApp.get(session, FileApp.id == app_id) - if not app: - http_exceptions.raise_not_found("应用不存在") + app = await FileApp.get_exist_one(session, app_id) # 检查 app_key 唯一性 if request.app_key is not None and request.app_key != app.app_key: @@ -235,9 +231,7 @@ async def delete_file_app( 错误处理: - 404: 应用不存在 """ - app: FileApp | None = await FileApp.get(session, FileApp.id == app_id) - if not app: - http_exceptions.raise_not_found("应用不存在") + app = await FileApp.get_exist_one(session, app_id) app_name = app.app_key await FileApp.delete(session, app) @@ -263,9 +257,7 @@ async def update_extensions( 错误处理: - 404: 应用不存在 """ - app: FileApp | None = await FileApp.get(session, FileApp.id == app_id) - if not app: - http_exceptions.raise_not_found("应用不存在") + app = await FileApp.get_exist_one(session, app_id) # 保留旧扩展名的 wopi_action_url(Discovery 填充的值) old_extensions: list[FileAppExtension] = await FileAppExtension.get( @@ -330,9 +322,7 @@ async def update_group_access( 错误处理: - 404: 应用不存在 """ - app: FileApp | None = await FileApp.get(session, FileApp.id == app_id) - if not app: - http_exceptions.raise_not_found("应用不存在") + await FileApp.get_exist_one(session, app_id) # 删除旧的用户组关联 old_links_result = await session.exec( @@ -387,9 +377,7 @@ async def discover_wopi( - 400: 非 WOPI 类型 / discovery URL 未配置 / XML 解析失败 - 502: WOPI 服务端不可达或返回无效响应 """ - app: FileApp | None = await FileApp.get(session, FileApp.id == app_id) - if not app: - http_exceptions.raise_not_found("应用不存在") + app = await FileApp.get_exist_one(session, app_id) if app.type != FileAppType.WOPI: http_exceptions.raise_bad_request("仅 WOPI 类型应用支持自动发现") diff --git a/routers/api/v1/admin/group/__init__.py b/routers/api/v1/admin/group/__init__.py index fd18dbc..e95977e 100644 --- a/routers/api/v1/admin/group/__init__.py +++ b/routers/api/v1/admin/group/__init__.py @@ -63,10 +63,7 @@ async def router_admin_get_group( :param group_id: 用户组UUID :return: 用户组详情 """ - group = await Group.get(session, Group.id == group_id, load=[Group.options, Group.policies]) - - if not group: - raise HTTPException(status_code=404, detail="用户组不存在") + group = await Group.get_exist_one(session, group_id, load=[Group.options, Group.policies]) # 直接访问已加载的关系,无需额外查询 policies = group.policies @@ -94,9 +91,7 @@ async def router_admin_get_group_members( :return: 分页成员列表 """ # 验证组存在 - group = await Group.get(session, Group.id == group_id) - if not group: - raise HTTPException(status_code=404, detail="用户组不存在") + await Group.get_exist_one(session, group_id) result = await User.get_with_count(session, User.group_id == group_id, table_view=table_view) @@ -138,10 +133,11 @@ async def router_admin_create_group( speed_limit=request.speed_limit, ) group = await group.save(session) + group_id_val: UUID = group.id # 创建选项 options = GroupOptions( - group_id=group.id, + group_id=group_id_val, share_download=request.share_download, share_free=request.share_free, relocate=request.relocate, @@ -154,11 +150,11 @@ async def router_admin_create_group( aria2=request.aria2, redirected_source=request.redirected_source, ) - await options.save(session) + options = await options.save(session) # 关联存储策略 for policy_id in request.policy_ids: - link = GroupPolicyLink(group_id=group.id, policy_id=policy_id) + link = GroupPolicyLink(group_id=group_id_val, policy_id=policy_id) session.add(link) await session.commit() @@ -185,9 +181,7 @@ async def router_admin_update_group( :param request: 更新请求 :return: 更新结果 """ - group = await Group.get(session, Group.id == group_id, load=Group.options) - if not group: - raise HTTPException(status_code=404, detail="用户组不存在") + group = await Group.get_exist_one(session, group_id, load=Group.options) # 检查名称唯一性(如果要更新名称) if request.name and request.name != group.name: @@ -217,7 +211,7 @@ async def router_admin_update_group( if options_data: for key, value in options_data.items(): setattr(group.options, key, value) - await group.options.save(session) + group.options = await group.options.save(session) # 更新策略关联 if request.policy_ids is not None: @@ -255,9 +249,7 @@ async def router_admin_delete_group( :param group_id: 用户组UUID :return: 删除结果 """ - group = await Group.get(session, Group.id == group_id) - if not group: - raise HTTPException(status_code=404, detail="用户组不存在") + group = await Group.get_exist_one(session, group_id) # 检查是否有用户属于该组 user_count = await User.count(session, User.group_id == group_id) diff --git a/routers/api/v1/admin/policy/__init__.py b/routers/api/v1/admin/policy/__init__.py index 8746d73..d248d71 100644 --- a/routers/api/v1/admin/policy/__init__.py +++ b/routers/api/v1/admin/policy/__init__.py @@ -332,7 +332,7 @@ async def router_policy_add_policy( s3_path_style=request.s3_path_style, s3_region=request.s3_region, ) - await options.save(session) + options = await options.save(session) @admin_policy_router.post( path='/cors', @@ -383,9 +383,7 @@ async def router_policy_onddrive_oauth( :param policy_id: 存储策略UUID :return: OAuth URL """ - policy = await Policy.get(session, Policy.id == policy_id) - if not policy: - raise HTTPException(status_code=404, detail="存储策略不存在") + policy = await Policy.get_exist_one(session, policy_id) # TODO: 实现OneDrive OAuth raise HTTPException(status_code=501, detail="OneDrive OAuth暂未实现") @@ -408,9 +406,7 @@ async def router_policy_get_policy( :param policy_id: 存储策略UUID :return: 策略详情 """ - policy = await Policy.get(session, Policy.id == policy_id, load=Policy.options) - if not policy: - raise HTTPException(status_code=404, detail="存储策略不存在") + policy = await Policy.get_exist_one(session, policy_id, load=Policy.options) # 获取使用此策略的用户组 groups = await policy.awaitable_attrs.groups @@ -459,9 +455,7 @@ async def router_policy_delete_policy( :param policy_id: 存储策略UUID :return: 删除结果 """ - policy = await Policy.get(session, Policy.id == policy_id) - if not policy: - raise HTTPException(status_code=404, detail="存储策略不存在") + policy = await Policy.get_exist_one(session, policy_id) # 检查是否有文件使用此策略 file_count = await Object.count(session, Object.policy_id == policy_id) @@ -503,9 +497,7 @@ async def router_policy_update_policy( :param policy_id: 存储策略UUID :param request: 更新请求 """ - policy = await Policy.get(session, Policy.id == policy_id, load=Policy.options) - if not policy: - raise HTTPException(status_code=404, detail="存储策略不存在") + policy = await Policy.get_exist_one(session, policy_id, load=Policy.options) # 检查名称唯一性(如果要更新名称) if request.name and request.name != policy.name: @@ -529,10 +521,10 @@ async def router_policy_update_policy( if policy.options: for key, value in options_data.items(): setattr(policy.options, key, value) - await policy.options.save(session) + policy.options = await policy.options.save(session) else: options = PolicyOptions(policy_id=policy.id, **options_data) - await options.save(session) + options = await options.save(session) l.info(f"管理员更新了存储策略: {policy_id}") diff --git a/routers/api/v1/admin/share/__init__.py b/routers/api/v1/admin/share/__init__.py index 9fa17dd..5a92b44 100644 --- a/routers/api/v1/admin/share/__init__.py +++ b/routers/api/v1/admin/share/__init__.py @@ -155,9 +155,7 @@ async def router_admin_delete_share( :param share_id: 分享ID :return: 删除结果 """ - share = await Share.get(session, Share.id == share_id) - if not share: - raise HTTPException(status_code=404, detail="分享不存在") + share = await Share.get_exist_one(session, share_id) await Share.delete(session, share) diff --git a/routers/api/v1/admin/task/__init__.py b/routers/api/v1/admin/task/__init__.py index d05ad41..1a8f533 100644 --- a/routers/api/v1/admin/task/__init__.py +++ b/routers/api/v1/admin/task/__init__.py @@ -150,9 +150,7 @@ async def router_admin_delete_task( :param task_id: 任务ID :return: 删除结果 """ - task = await Task.get(session, Task.id == task_id) - if not task: - raise HTTPException(status_code=404, detail="任务不存在") + task = await Task.get_exist_one(session, task_id) await Task.delete(session, task) diff --git a/routers/api/v1/admin/theme/__init__.py b/routers/api/v1/admin/theme/__init__.py index 677c95d..8df53e5 100644 --- a/routers/api/v1/admin/theme/__init__.py +++ b/routers/api/v1/admin/theme/__init__.py @@ -71,7 +71,7 @@ async def router_admin_theme_create( name=request.name, **request.colors.model_dump(), ) - await preset.save(session) + preset = await preset.save(session) l.info(f"管理员创建了主题预设: {request.name}") @@ -101,11 +101,7 @@ async def router_admin_theme_update( - 404: 预设不存在 - 409: 名称已被其他预设使用 """ - preset: ThemePreset | None = await ThemePreset.get( - session, ThemePreset.id == preset_id - ) - if not preset: - http_exceptions.raise_not_found("主题预设不存在") + preset = await ThemePreset.get_exist_one(session, preset_id) # 检查名称唯一性(排除自身) if request.name is not None and request.name != preset.name: @@ -120,7 +116,7 @@ async def router_admin_theme_update( for key, value in color_data.items(): setattr(preset, key, value) - await preset.save(session) + preset = await preset.save(session) l.info(f"管理员更新了主题预设: {preset.name}") @@ -147,11 +143,7 @@ async def router_admin_theme_delete( 副作用: - 关联用户的 theme_preset_id 会被数据库 SET NULL """ - preset: ThemePreset | None = await ThemePreset.get( - session, ThemePreset.id == preset_id - ) - if not preset: - http_exceptions.raise_not_found("主题预设不存在") + preset = await ThemePreset.get_exist_one(session, preset_id) await preset.delete(session) l.info(f"管理员删除了主题预设: {preset.name}") @@ -180,11 +172,7 @@ async def router_admin_theme_set_default( 逻辑: - 事务中先清除所有旧默认,再设新默认 """ - preset: ThemePreset | None = await ThemePreset.get( - session, ThemePreset.id == preset_id - ) - if not preset: - http_exceptions.raise_not_found("主题预设不存在") + preset = await ThemePreset.get_exist_one(session, preset_id) # 清除所有旧默认 await session.execute( @@ -195,5 +183,5 @@ async def router_admin_theme_set_default( # 设新默认 preset.is_default = True - await preset.save(session) + preset = await preset.save(session) l.info(f"管理员将主题预设 '{preset.name}' 设为默认") diff --git a/routers/api/v1/admin/user/__init__.py b/routers/api/v1/admin/user/__init__.py index f26df50..b18dec0 100644 --- a/routers/api/v1/admin/user/__init__.py +++ b/routers/api/v1/admin/user/__init__.py @@ -128,8 +128,9 @@ async def router_admin_create_user( is_verified=True, user_id=user.id, ) - await identity.save(session) + identity = await identity.save(session) + user = await User.get(session, User.id == user.id, load=User.group) return user.to_public() @@ -153,9 +154,7 @@ async def router_admin_update_user( :param request: 更新请求 :return: 更新结果 """ - user = await User.get(session, User.id == user_id) - if not user: - raise HTTPException(status_code=404, detail="用户不存在") + user = await User.get_exist_one(session, user_id) # 默认管理员不允许更改用户组(通过 Setting 中的 default_admin_id 识别) default_admin_setting = await Setting.get( @@ -252,9 +251,7 @@ async def router_admin_calibrate_storage( :param user_id: 用户UUID :return: 校准结果 """ - user = await User.get(session, User.id == user_id) - if not user: - raise HTTPException(status_code=404, detail="用户不存在") + user = await User.get_exist_one(session, user_id) previous_storage = user.storage diff --git a/routers/api/v1/admin/vas/__init__.py b/routers/api/v1/admin/vas/__init__.py deleted file mode 100644 index 8de1ba8..0000000 --- a/routers/api/v1/admin/vas/__init__.py +++ /dev/null @@ -1,81 +0,0 @@ -from uuid import UUID - -from fastapi import APIRouter, Depends, HTTPException - -from middleware.auth import admin_required -from middleware.dependencies import SessionDep -from sqlmodels import ( - ResponseBase, -) - -admin_vas_router = APIRouter( - prefix='/vas', - tags=['admin', 'admin_vas'] -) - -@admin_vas_router.get( - path='/list', - summary='获取增值服务列表', - description='Get VAS list (orders and storage packs)', - dependencies=[Depends(admin_required)] -) -async def router_admin_get_vas_list( - session: SessionDep, - user_id: UUID | None = None, - page: int = 1, - page_size: int = 20, -) -> ResponseBase: - """ - 获取增值服务列表(订单和存储包)。 - - :param session: 数据库会话 - :param user_id: 按用户筛选 - :param page: 页码 - :param page_size: 每页数量 - :return: 增值服务列表 - """ - # TODO: 实现增值服务列表 - # 需要查询 Order 和 StoragePack 模型 - raise HTTPException(status_code=501, detail="增值服务管理暂未实现") - - -@admin_vas_router.get( - path='/{vas_id}', - summary='获取增值服务详情', - description='Get VAS detail by ID', - dependencies=[Depends(admin_required)] -) -async def router_admin_get_vas( - session: SessionDep, - vas_id: UUID, -) -> ResponseBase: - """ - 获取增值服务详情。 - - :param session: 数据库会话 - :param vas_id: 增值服务UUID - :return: 增值服务详情 - """ - # TODO: 实现增值服务详情 - raise HTTPException(status_code=501, detail="增值服务管理暂未实现") - - -@admin_vas_router.delete( - path='/{vas_id}', - summary='删除增值服务', - description='Delete VAS by ID', - dependencies=[Depends(admin_required)] -) -async def router_admin_delete_vas( - session: SessionDep, - vas_id: UUID, -) -> ResponseBase: - """ - 删除增值服务。 - - :param session: 数据库会话 - :param vas_id: 增值服务UUID - :return: 删除结果 - """ - # TODO: 实现增值服务删除 - raise HTTPException(status_code=501, detail="增值服务管理暂未实现") \ No newline at end of file diff --git a/routers/api/v1/callback/__init__.py b/routers/api/v1/callback/__init__.py index d57a9c4..5ec6d17 100644 --- a/routers/api/v1/callback/__init__.py +++ b/routers/api/v1/callback/__init__.py @@ -16,18 +16,12 @@ oauth_router = APIRouter( tags=["callback", "oauth"], ) -pay_router = APIRouter( - prefix='/callback/pay', - tags=["callback", "pay"], -) - upload_router = APIRouter( prefix='/callback/upload', tags=["callback", "upload"], ) callback_router.include_router(oauth_router) -callback_router.include_router(pay_router) callback_router.include_router(upload_router) @oauth_router.post( @@ -38,7 +32,7 @@ callback_router.include_router(upload_router) def router_callback_qq() -> ResponseBase: """ Handle QQ OAuth callback and return user information. - + Returns: ResponseBase: A model containing the response data for the QQ OAuth callback. """ @@ -55,11 +49,11 @@ async def router_callback_github( GitHub OAuth 回调处理 - 错误响应示例: - { - 'error': 'bad_verification_code', - 'error_description': 'The code passed is incorrect or expired.', + 'error': 'bad_verification_code', + 'error_description': 'The code passed is incorrect or expired.', 'error_uri': 'https://docs.github.com/apps/managing-oauth-apps/troubleshooting-oauth-app-access-token-request-errors/#bad-verification-code' } - + Returns: PlainTextResponse: A response containing the user information from GitHub. """ @@ -77,81 +71,6 @@ async def router_callback_github( l.error(f"GitHub OAuth 回调异常: {e}") return PlainTextResponse("认证过程中发生错误,请重试", status_code=500) -@pay_router.post( - path='/alipay', - summary='支付宝支付回调', - description='Handle Alipay payment callback and return payment status.', -) -def router_callback_alipay() -> ResponseBase: - """ - Handle Alipay payment callback and return payment status. - - Returns: - ResponseBase: A model containing the response data for the Alipay payment callback. - """ - http_exceptions.raise_not_implemented() - -@pay_router.post( - path='/wechat', - summary='微信支付回调', - description='Handle WeChat Pay payment callback and return payment status.', -) -def router_callback_wechat() -> ResponseBase: - """ - Handle WeChat Pay payment callback and return payment status. - - Returns: - ResponseBase: A model containing the response data for the WeChat Pay payment callback. - """ - http_exceptions.raise_not_implemented() - -@pay_router.post( - path='/stripe', - summary='Stripe支付回调', - description='Handle Stripe payment callback and return payment status.', -) -def router_callback_stripe() -> ResponseBase: - """ - Handle Stripe payment callback and return payment status. - - Returns: - ResponseBase: A model containing the response data for the Stripe payment callback. - """ - http_exceptions.raise_not_implemented() - -@pay_router.get( - path='/easypay', - summary='易支付回调', - description='Handle EasyPay payment callback and return payment status.', -) -def router_callback_easypay() -> PlainTextResponse: - """ - Handle EasyPay payment callback and return payment status. - - Returns: - PlainTextResponse: A response containing the payment status for the EasyPay payment callback. - """ - http_exceptions.raise_not_implemented() - # return PlainTextResponse("success", status_code=200) - -@pay_router.get( - path='/custom/{order_no}/{id}', - summary='自定义支付回调', - description='Handle custom payment callback and return payment status.', -) -def router_callback_custom(order_no: str, id: str) -> ResponseBase: - """ - Handle custom payment callback and return payment status. - - Args: - order_no (str): The order number for the payment. - id (str): The ID associated with the payment. - - Returns: - ResponseBase: A model containing the response data for the custom payment callback. - """ - http_exceptions.raise_not_implemented() - @upload_router.post( path='/remote/{session_id}/{key}', summary='远程上传回调', @@ -160,11 +79,11 @@ def router_callback_custom(order_no: str, id: str) -> ResponseBase: def router_callback_remote(session_id: str, key: str) -> ResponseBase: """ Handle remote upload callback and return upload status. - + Args: session_id (str): The session ID for the upload. key (str): The key for the uploaded file. - + Returns: ResponseBase: A model containing the response data for the remote upload callback. """ @@ -178,15 +97,15 @@ def router_callback_remote(session_id: str, key: str) -> ResponseBase: def router_callback_qiniu(session_id: str) -> ResponseBase: """ Handle Qiniu Cloud upload callback and return upload status. - + Args: session_id (str): The session ID for the upload. - + Returns: ResponseBase: A model containing the response data for the Qiniu Cloud upload callback. """ http_exceptions.raise_not_implemented() - + @upload_router.post( path='/tencent/{session_id}', summary='腾讯云上传回调', @@ -195,16 +114,16 @@ def router_callback_qiniu(session_id: str) -> ResponseBase: def router_callback_tencent(session_id: str) -> ResponseBase: """ Handle Tencent Cloud upload callback and return upload status. - + Args: session_id (str): The session ID for the upload. - + Returns: ResponseBase: A model containing the response data for the Tencent Cloud upload callback. """ http_exceptions.raise_not_implemented() -@upload_router.post( +@upload_router.post( path='/aliyun/{session_id}', summary='阿里云上传回调', description='Handle Aliyun upload callback and return upload status.', @@ -212,16 +131,16 @@ def router_callback_tencent(session_id: str) -> ResponseBase: def router_callback_aliyun(session_id: str) -> ResponseBase: """ Handle Aliyun upload callback and return upload status. - + Args: session_id (str): The session ID for the upload. - + Returns: ResponseBase: A model containing the response data for the Aliyun upload callback. """ http_exceptions.raise_not_implemented() -@upload_router.post( +@upload_router.post( path='/upyun/{session_id}', summary='又拍云上传回调', description='Handle Upyun upload callback and return upload status.', @@ -229,10 +148,10 @@ def router_callback_aliyun(session_id: str) -> ResponseBase: def router_callback_upyun(session_id: str) -> ResponseBase: """ Handle Upyun upload callback and return upload status. - + Args: session_id (str): The session ID for the upload. - + Returns: ResponseBase: A model containing the response data for the Upyun upload callback. """ @@ -246,10 +165,10 @@ def router_callback_upyun(session_id: str) -> ResponseBase: def router_callback_aws(session_id: str) -> ResponseBase: """ Handle AWS S3 upload callback and return upload status. - + Args: session_id (str): The session ID for the upload. - + Returns: ResponseBase: A model containing the response data for the AWS S3 upload callback. """ @@ -263,10 +182,10 @@ def router_callback_aws(session_id: str) -> ResponseBase: def router_callback_onedrive_finish(session_id: str) -> ResponseBase: """ Handle OneDrive upload completion callback and return upload status. - + Args: session_id (str): The session ID for the upload. - + Returns: ResponseBase: A model containing the response data for the OneDrive upload completion callback. """ @@ -280,7 +199,7 @@ def router_callback_onedrive_finish(session_id: str) -> ResponseBase: def router_callback_onedrive_auth() -> ResponseBase: """ Handle OneDrive authorization callback and return authorization status. - + Returns: ResponseBase: A model containing the response data for the OneDrive authorization callback. """ @@ -294,8 +213,8 @@ def router_callback_onedrive_auth() -> ResponseBase: def router_callback_google_auth() -> ResponseBase: """ Handle Google OAuth completion callback and return authorization status. - + Returns: ResponseBase: A model containing the response data for the Google OAuth completion callback. """ - http_exceptions.raise_not_implemented() \ No newline at end of file + http_exceptions.raise_not_implemented() diff --git a/routers/api/v1/category/__init__.py b/routers/api/v1/category/__init__.py new file mode 100644 index 0000000..d3965e1 --- /dev/null +++ b/routers/api/v1/category/__init__.py @@ -0,0 +1,100 @@ +""" +文件分类筛选端点 + +按文件类型分类(图片/视频/音频/文档)查询用户的所有文件, +跨目录搜索,支持分页。扩展名映射从数据库 Setting 表读取。 +""" +from typing import Annotated + +from fastapi import APIRouter, Depends, HTTPException +from loguru import logger as l + +from middleware.auth import auth_required +from middleware.dependencies import SessionDep, TableViewRequestDep +from sqlmodels import ( + FileCategory, + ListResponse, + Object, + ObjectResponse, + ObjectType, + Setting, + SettingsType, + User, +) + +category_router = APIRouter( + prefix="/category", + tags=["category"], +) + + +@category_router.get( + path="/{category}", + summary="按分类获取文件列表", +) +async def router_category_list( + session: SessionDep, + user: Annotated[User, Depends(auth_required)], + category: FileCategory, + table_view: TableViewRequestDep, +) -> ListResponse[ObjectResponse]: + """ + 按文件类型分类查询用户的所有文件 + + 跨所有目录搜索,返回分页结果。 + 扩展名配置从数据库 Setting 表读取(type=file_category)。 + + 认证: + - JWT token in Authorization header + + 路径参数: + - category: 文件分类(image / video / audio / document) + + 查询参数: + - offset: 分页偏移量(默认0) + - limit: 每页数量(默认20,最大100) + - desc: 是否降序(默认true) + - order: 排序字段(created_at / updated_at) + + 响应: + - ListResponse[ObjectResponse]: 分页文件列表 + + 错误处理: + - HTTPException 422: category 参数无效 + - HTTPException 404: 该分类未配置扩展名 + """ + # 从数据库读取该分类的扩展名配置 + setting = await Setting.get( + session, + (Setting.type == SettingsType.FILE_CATEGORY) & (Setting.name == category.value), + ) + if not setting or not setting.value: + raise HTTPException(status_code=404, detail=f"分类 {category.value} 未配置扩展名") + + extensions = [ext.strip() for ext in setting.value.split(",") if ext.strip()] + if not extensions: + raise HTTPException(status_code=404, detail=f"分类 {category.value} 扩展名列表为空") + + result = await Object.get_by_category( + session, + user.id, + extensions, + table_view=table_view, + ) + + items = [ + ObjectResponse( + id=obj.id, + name=obj.name, + type=ObjectType.FILE, + size=obj.size, + mime_type=obj.mime_type, + thumb=False, + created_at=obj.created_at, + updated_at=obj.updated_at, + source_enabled=False, + ) + for obj in result.items + ] + + return ListResponse(count=result.count, items=items) diff --git a/routers/api/v1/directory/__init__.py b/routers/api/v1/directory/__init__.py index ca7218f..084380f 100644 --- a/routers/api/v1/directory/__init__.py +++ b/routers/api/v1/directory/__init__.py @@ -206,4 +206,4 @@ async def router_directory_create( parent_id=parent_id, policy_id=policy_id, ) - await new_folder.save(session) + new_folder = await new_folder.save(session) diff --git a/routers/api/v1/file/__init__.py b/routers/api/v1/file/__init__.py index e618cfd..7cfed84 100644 --- a/routers/api/v1/file/__init__.py +++ b/routers/api/v1/file/__init__.py @@ -184,9 +184,7 @@ async def create_upload_session( # 确定存储策略 policy_id = request.policy_id or parent.policy_id - policy = await Policy.get(session, Policy.id == policy_id) - if not policy: - raise HTTPException(status_code=404, detail="存储策略不存在") + policy = await Policy.get_exist_one(session, policy_id) # 校验用户组是否有权使用该策略(仅当用户显式指定 policy_id 时) if request.policy_id: @@ -711,9 +709,7 @@ async def create_empty_file( # 确定存储策略 policy_id = request.policy_id or parent.policy_id - policy = await Policy.get(session, Policy.id == policy_id) - if not policy: - raise HTTPException(status_code=404, detail="存储策略不存在") + policy = await Policy.get_exist_one(session, policy_id) # 生成存储路径并创建空文件 storage_path: str | None = None @@ -942,7 +938,7 @@ async def file_get( # 递增下载次数 link.downloads += 1 - await link.save(session) + link = await link.save(session) if policy.type == PolicyType.LOCAL: storage_service = LocalStorageService(policy) @@ -996,7 +992,7 @@ async def file_source_redirect( # 递增下载次数 link.downloads += 1 - await link.save(session) + link = await link.save(session) if policy.type == PolicyType.LOCAL: storage_service = LocalStorageService(policy) diff --git a/routers/api/v1/object/__init__.py b/routers/api/v1/object/__init__.py index 322a83d..ecc2c0b 100644 --- a/routers/api/v1/object/__init__.py +++ b/routers/api/v1/object/__init__.py @@ -112,9 +112,7 @@ async def router_object_create( # 确定存储策略 policy_id = request.policy_id or parent.policy_id - policy = await Policy.get(session, Policy.id == policy_id) - if not policy: - raise HTTPException(status_code=404, detail="存储策略不存在") + policy = await Policy.get_exist_one(session, policy_id) parent_id = parent.id @@ -149,7 +147,7 @@ async def router_object_create( owner_id=user_id, policy_id=policy_id, ) - await file_object.save(session) + file_object = await file_object.save(session) l.info(f"创建空白文件: {request.name}") @@ -474,7 +472,7 @@ async def router_object_rename( # 更新名称 obj.name = new_name - await obj.save(session) + obj = await obj.save(session) l.info(f"用户 {user_id} 将对象 {obj.id} 重命名为 {new_name}") @@ -682,7 +680,7 @@ async def router_object_switch_policy( dest_policy_id=dest_policy_id, object_id=obj_id, ) - await task_props.save(session) + task_props = await task_props.save(session) if obj_is_file: # 文件:后台迁移 @@ -698,7 +696,7 @@ async def router_object_switch_policy( # 目录:先更新目录自身的 policy_id obj = await Object.get(session, Object.id == obj_id) obj.policy_id = dest_policy_id - await obj.save(session) + obj = await obj.save(session) if request.is_migrate_existing: # 后台迁移所有已有文件 @@ -715,7 +713,7 @@ async def router_object_switch_policy( task = await Task.get(session, Task.id == task_id) task.status = TaskStatus.COMPLETED task.progress = 100 - await task.save(session) + task = await task.save(session) # 重新获取 task 以读取最新状态 task = await Task.get(session, Task.id == task_id) @@ -850,7 +848,7 @@ async def router_patch_object_metadata( ) if existing: existing.value = patch.value - await existing.save(session) + existing = await existing.save(session) else: entry = ObjectMetadata( object_id=object_id, @@ -858,6 +856,6 @@ async def router_patch_object_metadata( value=patch.value, is_public=True, ) - await entry.save(session) + entry = await entry.save(session) l.info(f"用户 {user.id} 更新了对象 {object_id} 的 {len(request.patches)} 条元数据") diff --git a/routers/api/v1/object/custom_property/__init__.py b/routers/api/v1/object/custom_property/__init__.py index 29f93d3..351f92b 100644 --- a/routers/api/v1/object/custom_property/__init__.py +++ b/routers/api/v1/object/custom_property/__init__.py @@ -102,7 +102,7 @@ async def router_create_custom_property( options=request.options, default_value=request.default_value, ) - await definition.save(session) + definition = await definition.save(session) l.info(f"用户 {user.id} 创建了自定义属性: {request.name}") @@ -128,12 +128,7 @@ async def router_update_custom_property( - 404: 属性定义不存在 - 403: 无权操作此属性 """ - definition = await CustomPropertyDefinition.get( - session, - CustomPropertyDefinition.id == id, - ) - if not definition: - raise HTTPException(status_code=404, detail="自定义属性不存在") + definition = await CustomPropertyDefinition.get_exist_one(session, id) if definition.owner_id != user.id: raise HTTPException(status_code=403, detail="无权操作此属性") @@ -163,12 +158,7 @@ async def router_delete_custom_property( - 404: 属性定义不存在 - 403: 无权操作此属性 """ - definition = await CustomPropertyDefinition.get( - session, - CustomPropertyDefinition.id == id, - ) - if not definition: - raise HTTPException(status_code=404, detail="自定义属性不存在") + definition = await CustomPropertyDefinition.get_exist_one(session, id) if definition.owner_id != user.id: raise HTTPException(status_code=403, detail="无权操作此属性") diff --git a/routers/api/v1/share/__init__.py b/routers/api/v1/share/__init__.py index f6bf5e5..762f4ba 100644 --- a/routers/api/v1/share/__init__.py +++ b/routers/api/v1/share/__init__.py @@ -45,12 +45,7 @@ async def router_share_get( 4. 返回分享详情(含文件树和分享者信息) """ # 1. 查询分享(预加载 user 和 object) - share = await Share.get( - session, Share.id == id, - load=[Share.user, Share.object], - ) - if not share: - http_exceptions.raise_not_found(detail="分享不存在或已被取消") + share = await Share.get_exist_one(session, id, load=[Share.user, Share.object]) # 2. 检查过期 now = datetime.now() @@ -474,16 +469,29 @@ def router_share_update(id: str) -> ResponseBase: path='/{id}', summary='删除分享', description='Delete a share by ID.', - dependencies=[Depends(auth_required)] + status_code=204, ) -def router_share_delete(id: str) -> ResponseBase: +async def router_share_delete( + session: SessionDep, + user: Annotated[User, Depends(auth_required)], + id: UUID, +) -> None: """ - Delete a share by ID. - - Args: - id (str): The ID of the share to be deleted. - - Returns: - ResponseBase: A model containing the response data for the deleted share. + 删除分享 + + 认证:需要 JWT token + + 流程: + 1. 通过分享ID查找分享 + 2. 验证分享属于当前用户 + 3. 删除分享记录 """ - http_exceptions.raise_not_implemented() \ No newline at end of file + share = await Share.get_exist_one(session, id) + if share.user_id != user.id: + http_exceptions.raise_forbidden(detail="无权删除此分享") + + user_id = user.id + share_code = share.code + await Share.delete(session, share) + + l.info(f"用户 {user_id} 删除了分享: {share_code}") \ No newline at end of file diff --git a/routers/api/v1/user/__init__.py b/routers/api/v1/user/__init__.py index fa718ba..545a0e7 100644 --- a/routers/api/v1/user/__init__.py +++ b/routers/api/v1/user/__init__.py @@ -234,7 +234,7 @@ async def router_user_register( group_id=default_group.id, ) new_user_id = new_user.id - await new_user.save(session) + new_user = await new_user.save(session) # 7. 创建 AuthIdentity hashed_password = Password.hash(request.credential) if request.credential else None @@ -246,7 +246,7 @@ async def router_user_register( is_verified=False, user_id=new_user_id, ) - await identity.save(session) + identity = await identity.save(session) # 8. 创建用户根目录(使用用户组关联的第一个存储策略) await session.refresh(default_group, ['policies']) @@ -494,9 +494,24 @@ async def router_user_storage( if not group: raise HTTPException(status_code=404, detail="用户组不存在") - # [TODO] 总空间加上用户购买的额外空间 + # 查询用户所有未过期容量包的 size 总和 + from datetime import datetime + from sqlalchemy import func, select, and_, or_ - total: int = group.max_storage + now = datetime.now() + stmt = select(func.coalesce(func.sum(sqlmodels.StoragePack.size), 0)).where( + and_( + sqlmodels.StoragePack.user_id == user.id, + or_( + sqlmodels.StoragePack.expired_time.is_(None), + sqlmodels.StoragePack.expired_time > now, + ), + ) + ) + result = await session.exec(stmt) + active_packs_total: int = result.scalar_one() + + total: int = group.max_storage + active_packs_total used: int = user.storage free: int = max(0, total - used) @@ -638,7 +653,7 @@ async def router_user_authn_finish( is_verified=True, user_id=user.id, ) - await identity.save(session) + identity = await identity.save(session) return authn.to_detail_response() diff --git a/routers/api/v1/user/settings/__init__.py b/routers/api/v1/user/settings/__init__.py index e1076e4..a875bb0 100644 --- a/routers/api/v1/user/settings/__init__.py +++ b/routers/api/v1/user/settings/__init__.py @@ -218,7 +218,7 @@ async def router_user_settings_avatar( # 更新用户头像字段 user.avatar = "file" - await user.save(session) + user = await user.save(session) @user_settings_router.put( @@ -252,7 +252,7 @@ async def router_user_settings_avatar_gravatar( await delete_avatar_files(session, user.id) user.avatar = "gravatar" - await user.save(session) + user = await user.save(session) @user_settings_router.delete( @@ -279,7 +279,7 @@ async def router_user_settings_avatar_delete( await delete_avatar_files(session, user.id) user.avatar = "default" - await user.save(session) + user = await user.save(session) @user_settings_router.patch( @@ -321,7 +321,7 @@ async def router_user_settings_theme( user.color_error = request.theme_colors.error user.color_neutral = request.theme_colors.neutral - await user.save(session) + user = await user.save(session) @user_settings_router.patch( @@ -358,7 +358,7 @@ async def router_user_settings_change_password( http_exceptions.raise_forbidden("当前密码错误") email_identity.credential = Password.hash(request.new_password) - await email_identity.save(session) + email_identity = await email_identity.save(session) @user_settings_router.patch( @@ -392,7 +392,7 @@ async def router_user_settings_patch( http_exceptions.raise_bad_request(f"设置项 {option.value} 不允许为空") setattr(user, option.value, value) - await user.save(session) + user = await user.save(session) @user_settings_router.get( @@ -454,7 +454,7 @@ async def router_user_settings_2fa_enable( extra: dict = orjson.loads(email_identity.extra_data) if email_identity.extra_data else {} extra["two_factor"] = secret email_identity.extra_data = orjson.dumps(extra).decode('utf-8') - await email_identity.save(session) + email_identity = await email_identity.save(session) # ==================== 认证身份管理 ==================== diff --git a/routers/api/v1/user/settings/file_viewers/__init__.py b/routers/api/v1/user/settings/file_viewers/__init__.py index 031fa2f..3c9e06b 100644 --- a/routers/api/v1/user/settings/file_viewers/__init__.py +++ b/routers/api/v1/user/settings/file_viewers/__init__.py @@ -79,9 +79,7 @@ async def set_default_viewer( if existing: existing.app_id = request.app_id - existing = await existing.save(session) - # 重新加载 app 关系 - await session.refresh(existing, attribute_names=["app"]) + existing = await existing.save(session, load=UserFileAppDefault.app) return existing.to_response() else: new_default = UserFileAppDefault( @@ -89,9 +87,7 @@ async def set_default_viewer( extension=normalized_ext, app_id=request.app_id, ) - new_default = await new_default.save(session) - # 重新加载 app 关系 - await session.refresh(new_default, attribute_names=["app"]) + new_default = await new_default.save(session, load=UserFileAppDefault.app) return new_default.to_response() diff --git a/routers/api/v1/vas/__init__.py b/routers/api/v1/vas/__init__.py deleted file mode 100644 index 6b43b6f..0000000 --- a/routers/api/v1/vas/__init__.py +++ /dev/null @@ -1,106 +0,0 @@ -from fastapi import APIRouter, Depends - -from middleware.auth import auth_required -from sqlmodels import ResponseBase -from utils import http_exceptions - -vas_router = APIRouter( - prefix="/vas", - tags=["vas"] -) - -@vas_router.get( - path='/pack', - summary='获取容量包及配额信息', - description='Get information about storage packs and quotas.', - dependencies=[Depends(auth_required)] -) -def router_vas_pack() -> ResponseBase: - """ - Get information about storage packs and quotas. - - Returns: - ResponseBase: A model containing the response data for storage packs and quotas. - """ - http_exceptions.raise_not_implemented() - -@vas_router.get( - path='/product', - summary='获取商品信息,同时返回支付信息', - description='Get product information along with payment details.', - dependencies=[Depends(auth_required)] -) -def router_vas_product() -> ResponseBase: - """ - Get product information along with payment details. - - Returns: - ResponseBase: A model containing the response data for products and payment information. - """ - http_exceptions.raise_not_implemented() - -@vas_router.post( - path='/order', - summary='新建支付订单', - description='Create an order for a product.', - dependencies=[Depends(auth_required)] -) -def router_vas_order() -> ResponseBase: - """ - Create an order for a product. - - Returns: - ResponseBase: A model containing the response data for the created order. - """ - http_exceptions.raise_not_implemented() - -@vas_router.get( - path='/order/{id}', - summary='查询订单状态', - description='Get information about a specific payment order by ID.', - dependencies=[Depends(auth_required)] -) -def router_vas_order_get(id: str) -> ResponseBase: - """ - Get information about a specific payment order by ID. - - Args: - id (str): The ID of the order to retrieve information for. - - Returns: - ResponseBase: A model containing the response data for the specified order. - """ - http_exceptions.raise_not_implemented() - -@vas_router.get( - path='/redeem', - summary='获取兑换码信息', - description='Get information about a specific redemption code.', - dependencies=[Depends(auth_required)] -) -def router_vas_redeem(code: str) -> ResponseBase: - """ - Get information about a specific redemption code. - - Args: - code (str): The redemption code to retrieve information for. - - Returns: - ResponseBase: A model containing the response data for the specified redemption code. - """ - http_exceptions.raise_not_implemented() - -@vas_router.post( - path='/redeem', - summary='执行兑换', - description='Redeem a redemption code for a product or service.', - dependencies=[Depends(auth_required)] -) -def router_vas_redeem_post() -> ResponseBase: - """ - Redeem a redemption code for a product or service. - - Returns: - ResponseBase: A model containing the response data for the redeemed code. - """ - http_exceptions.raise_not_implemented() \ No newline at end of file diff --git a/routers/dav/provider.py b/routers/dav/provider.py index 5c56adc..9e0a791 100644 --- a/routers/dav/provider.py +++ b/routers/dav/provider.py @@ -175,8 +175,7 @@ async def _finalize_upload( obj = await Object.get(session, Object.id == object_id) if obj: obj.sqlmodel_update({'size': size, 'physical_file_id': pf.id}) - session.add(obj) - await session.commit() + obj = await obj.save(session) # 更新用户存储用量 if size > 0: @@ -193,8 +192,7 @@ async def _move_object( obj = await Object.get(session, Object.id == object_id) if obj: obj.sqlmodel_update({'parent_id': new_parent_id, 'name': new_name}) - session.add(obj) - await session.commit() + obj = await obj.save(session) async def _copy_object_recursive( diff --git a/service/storage/migrate.py b/service/storage/migrate.py index 0cbab60..d767262 100644 --- a/service/storage/migrate.py +++ b/service/storage/migrate.py @@ -147,7 +147,7 @@ async def migrate_single_file( # 4. 更新 Object obj.policy_id = dest_policy.id obj.physical_file_id = new_physical.id - await obj.save(session) + obj = await obj.save(session) # 5. 旧 PhysicalFile 引用计数 -1 old_physical.decrement_reference() @@ -159,7 +159,7 @@ async def migrate_single_file( l.warning(f"删除源文件失败(不影响迁移结果): {old_physical.storage_path}: {e}") await PhysicalFile.delete(session, old_physical) else: - await old_physical.save(session) + old_physical = await old_physical.save(session) l.info(f"文件迁移完成: {obj.name} ({obj.id}), {src_policy.name} → {dest_policy.name}") @@ -187,12 +187,12 @@ async def migrate_file_with_task( task.status = TaskStatus.COMPLETED task.progress = 100 - await task.save(session) + task = await task.save(session) except Exception as e: l.error(f"文件迁移任务失败: {obj.id}: {e}") task.status = TaskStatus.ERROR task.error = str(e)[:500] - await task.save(session) + task = await task.save(session) async def migrate_directory_files( @@ -244,7 +244,7 @@ async def migrate_directory_files( # 更新所有子目录的 policy_id for sub_folder in folders_to_update: sub_folder.policy_id = dest_policy.id - await sub_folder.save(session) + sub_folder = await sub_folder.save(session) # 完成任务 if errors: @@ -254,7 +254,7 @@ async def migrate_directory_files( task.status = TaskStatus.COMPLETED task.progress = 100 - await task.save(session) + task = await task.save(session) l.info( f"目录迁移完成: {folder.name} ({folder.id}), " @@ -264,7 +264,7 @@ async def migrate_directory_files( l.error(f"目录迁移任务失败: {folder.id}: {e}") task.status = TaskStatus.ERROR task.error = str(e)[:500] - await task.save(session) + task = await task.save(session) async def _collect_objects_recursive( diff --git a/service/storage/object.py b/service/storage/object.py index 23f3803..42e67ae 100644 --- a/service/storage/object.py +++ b/service/storage/object.py @@ -287,7 +287,7 @@ async def permanently_delete_objects( await PhysicalFile.delete(session, physical_file, commit=False) l.debug(f"物理文件记录已删除: {physical_file.storage_path}") else: - await physical_file.save(session, commit=False) + physical_file = await physical_file.save(session, commit=False) l.debug(f"物理文件仍有 {physical_file.reference_count} 个引用: {physical_file.storage_path}") # 更新用户存储配额 @@ -399,7 +399,7 @@ async def delete_object_recursive( await PhysicalFile.delete(session, physical_file, commit=False) l.debug(f"物理文件记录已删除: {physical_file.storage_path}") else: - await physical_file.save(session, commit=False) + physical_file = await physical_file.save(session, commit=False) l.debug(f"物理文件仍有 {physical_file.reference_count} 个引用: {physical_file.storage_path}") # 阶段三:更新用户存储配额(与删除在同一事务中) @@ -458,7 +458,7 @@ async def _copy_object_recursive( physical_file = await PhysicalFile.get(session, PhysicalFile.id == src_physical_file_id) if physical_file: physical_file.increment_reference() - await physical_file.save(session) + physical_file = await physical_file.save(session) total_copied_size += src_size new_obj = await new_obj.save(session) diff --git a/service/user/login.py b/service/user/login.py index 4b97002..5f97c9a 100644 --- a/service/user/login.py +++ b/service/user/login.py @@ -192,7 +192,7 @@ async def _login_oauth( # 已绑定 → 更新 OAuth 信息并返回关联用户 identity.display_name = nickname identity.avatar_url = avatar_url - await identity.save(session) + identity = await identity.save(session) user: User = await User.get(session, User.id == identity.user_id, load=User.group) if not user: @@ -254,7 +254,7 @@ async def _auto_register_oauth_user( is_verified=True, user_id=new_user_id, ) - await identity.save(session) + identity = await identity.save(session) # 创建用户根目录 default_policy = await Policy.get(session, Policy.name == "本地存储") @@ -335,7 +335,7 @@ async def _login_passkey( # 更新签名计数 authn.sign_count = verification.new_sign_count - await authn.save(session) + authn = await authn.save(session) # 加载用户 user: User = await User.get(session, User.id == authn.user_id, load=User.group) @@ -392,7 +392,7 @@ async def _login_magic_link( # 标记邮箱已验证 if not identity.is_verified: identity.is_verified = True - await identity.save(session) + identity = await identity.save(session) return user diff --git a/setup_cython.py b/setup_cython.py index bcfa5a9..991d8d7 100644 --- a/setup_cython.py +++ b/setup_cython.py @@ -84,6 +84,7 @@ if __name__ == "__main__": setup( name="disknext-ee", + packages=[], ext_modules=cythonize( extensions, compiler_directives={'language_level': "3"}, diff --git a/sqlmodels/__init__.py b/sqlmodels/__init__.py index 79a1d09..8787101 100644 --- a/sqlmodels/__init__.py +++ b/sqlmodels/__init__.py @@ -82,6 +82,7 @@ from .object import ( ObjectResponse, ObjectSwitchPolicyRequest, ObjectType, + FileCategory, PolicyResponse, UploadChunkResponse, UploadSession, @@ -116,12 +117,22 @@ from .custom_property import ( ) from .physical_file import PhysicalFile, PhysicalFileBase from .uri import DiskNextURI, FileSystemNamespace -from .order import Order, OrderStatus, OrderType +from .order import ( + Order, OrderStatus, OrderType, + CreateOrderRequest, OrderResponse, +) from .policy import ( Policy, PolicyBase, PolicyCreateRequest, PolicyOptions, PolicyOptionsBase, PolicyType, PolicySummary, PolicyUpdateRequest, ) -from .redeem import Redeem, RedeemType +from .product import ( + Product, ProductBase, ProductType, PaymentMethod, + ProductCreateRequest, ProductUpdateRequest, ProductResponse, +) +from .redeem import ( + Redeem, RedeemType, + RedeemCreateRequest, RedeemUseRequest, RedeemInfoResponse, RedeemAdminResponse, +) from .report import Report, ReportReason from .setting import ( Setting, SettingsType, SiteConfigResponse, AuthMethodConfig, @@ -134,7 +145,7 @@ from .share import ( AdminShareListItem, ) from .source_link import SourceLink -from .storage_pack import StoragePack +from .storage_pack import StoragePack, StoragePackResponse from .tag import Tag, TagType from .task import Task, TaskProps, TaskPropsBase, TaskStatus, TaskType, TaskSummary, TaskSummaryBase from .webdav import ( diff --git a/sqlmodels/auth_identity.py b/sqlmodels/auth_identity.py index de1353b..4900b54 100644 --- a/sqlmodels/auth_identity.py +++ b/sqlmodels/auth_identity.py @@ -10,7 +10,7 @@ from uuid import UUID from sqlmodel import Field, Relationship, UniqueConstraint -from sqlmodel_ext import SQLModelBase, UUIDTableBaseMixin +from sqlmodel_ext import SQLModelBase, UUIDTableBaseMixin, Str100, Str128, Str255, Text1024 if TYPE_CHECKING: from .user import User @@ -87,7 +87,7 @@ class ChangePasswordRequest(SQLModelBase): old_password: str = Field(min_length=1) """当前密码""" - new_password: str = Field(min_length=8, max_length=128) + new_password: Str128 = Field(min_length=8) """新密码(至少 8 位)""" @@ -103,13 +103,13 @@ class AuthIdentity(SQLModelBase, UUIDTableBaseMixin): provider: AuthProviderType = Field(index=True) """提供者类型""" - identifier: str = Field(max_length=255, index=True) + identifier: Str255 = Field(index=True) """标识符(邮箱/手机号/OAuth openid)""" - credential: str | None = Field(default=None, max_length=1024) + credential: Text1024 | None = None """凭证(Argon2 哈希密码 / null)""" - display_name: str | None = Field(default=None, max_length=100) + display_name: Str100 | None = None """OAuth 昵称""" avatar_url: str | None = Field(default=None, max_length=512) diff --git a/sqlmodels/custom_property.py b/sqlmodels/custom_property.py index e8a251f..ddf84bc 100644 --- a/sqlmodels/custom_property.py +++ b/sqlmodels/custom_property.py @@ -13,7 +13,7 @@ from uuid import UUID from sqlalchemy import JSON from sqlmodel import Field, Relationship -from sqlmodel_ext import SQLModelBase, UUIDTableBaseMixin +from sqlmodel_ext import SQLModelBase, UUIDTableBaseMixin, Str100 if TYPE_CHECKING: from .user import User @@ -44,13 +44,13 @@ class CustomPropertyType(StrEnum): class CustomPropertyDefinitionBase(SQLModelBase): """自定义属性定义基础模型""" - name: str = Field(max_length=100) + name: Str100 """属性显示名称""" type: CustomPropertyType """属性值类型""" - icon: str | None = Field(default=None, max_length=100) + icon: Str100 | None = None """图标标识(iconify 名称)""" options: list[str] | None = Field(default=None, sa_type=JSON) @@ -90,7 +90,7 @@ class CustomPropertyDefinition(CustomPropertyDefinitionBase, UUIDTableBaseMixin) class CustomPropertyCreateRequest(SQLModelBase): """创建自定义属性请求 DTO""" - name: str = Field(max_length=100) + name: Str100 """属性显示名称""" type: CustomPropertyType diff --git a/sqlmodels/download.py b/sqlmodels/download.py index 671a663..b6f1d4b 100644 --- a/sqlmodels/download.py +++ b/sqlmodels/download.py @@ -4,7 +4,7 @@ from uuid import UUID from sqlmodel import Field, Relationship, UniqueConstraint, Index -from sqlmodel_ext import SQLModelBase, UUIDTableBaseMixin, TableBaseMixin +from sqlmodel_ext import SQLModelBase, UUIDTableBaseMixin, TableBaseMixin, Str255 if TYPE_CHECKING: from .user import User @@ -141,7 +141,7 @@ class Download(DownloadBase, UUIDTableBaseMixin): speed: int = Field(default=0) """下载速度(bytes/s)""" - parent: str | None = Field(default=None, max_length=255) + parent: Str255 | None = None """父任务标识""" error: str | None = Field(default=None) diff --git a/sqlmodels/file_app.py b/sqlmodels/file_app.py index 0fc8cf3..5364ba4 100644 --- a/sqlmodels/file_app.py +++ b/sqlmodels/file_app.py @@ -20,7 +20,7 @@ from uuid import UUID from sqlmodel import Field, Relationship, UniqueConstraint -from sqlmodel_ext import SQLModelBase, TableBaseMixin, UUIDTableBaseMixin +from sqlmodel_ext import SQLModelBase, TableBaseMixin, UUIDTableBaseMixin, Str100, Str255, Text1024 if TYPE_CHECKING: from .group import Group @@ -119,7 +119,7 @@ class UserFileAppDefaultResponse(SQLModelBase): class FileAppCreateRequest(SQLModelBase): """管理员创建应用请求 DTO""" - name: str = Field(max_length=100) + name: Str100 """应用名称""" app_key: str = Field(max_length=50) @@ -128,7 +128,7 @@ class FileAppCreateRequest(SQLModelBase): type: FileAppType """应用类型""" - icon: str | None = Field(default=None, max_length=255) + icon: Str255 | None = None """图标名称/URL""" description: str | None = Field(default=None, max_length=500) @@ -140,13 +140,13 @@ class FileAppCreateRequest(SQLModelBase): is_restricted: bool = False """是否限制用户组访问""" - iframe_url_template: str | None = Field(default=None, max_length=1024) + iframe_url_template: Text1024 | None = None """iframe URL 模板""" wopi_discovery_url: str | None = Field(default=None, max_length=512) """WOPI 发现端点 URL""" - wopi_editor_url_template: str | None = Field(default=None, max_length=1024) + wopi_editor_url_template: Text1024 | None = None """WOPI 编辑器 URL 模板""" extensions: list[str] = [] @@ -159,7 +159,7 @@ class FileAppCreateRequest(SQLModelBase): class FileAppUpdateRequest(SQLModelBase): """管理员更新应用请求 DTO(所有字段可选)""" - name: str | None = Field(default=None, max_length=100) + name: Str100 | None = None """应用名称""" app_key: str | None = Field(default=None, max_length=50) @@ -168,7 +168,7 @@ class FileAppUpdateRequest(SQLModelBase): type: FileAppType | None = None """应用类型""" - icon: str | None = Field(default=None, max_length=255) + icon: Str255 | None = None """图标名称/URL""" description: str | None = Field(default=None, max_length=500) @@ -180,13 +180,13 @@ class FileAppUpdateRequest(SQLModelBase): is_restricted: bool | None = None """是否限制用户组访问""" - iframe_url_template: str | None = Field(default=None, max_length=1024) + iframe_url_template: Text1024 | None = None """iframe URL 模板""" wopi_discovery_url: str | None = Field(default=None, max_length=512) """WOPI 发现端点 URL""" - wopi_editor_url_template: str | None = Field(default=None, max_length=1024) + wopi_editor_url_template: Text1024 | None = None """WOPI 编辑器 URL 模板""" @@ -325,7 +325,7 @@ class WopiDiscoveryResponse(SQLModelBase): class FileApp(SQLModelBase, UUIDTableBaseMixin): """文件查看器应用注册表""" - name: str = Field(max_length=100) + name: Str100 """应用名称""" app_key: str = Field(max_length=50, unique=True, index=True) @@ -334,7 +334,7 @@ class FileApp(SQLModelBase, UUIDTableBaseMixin): type: FileAppType """应用类型""" - icon: str | None = Field(default=None, max_length=255) + icon: Str255 | None = None """图标名称/URL""" description: str | None = Field(default=None, max_length=500) @@ -346,13 +346,13 @@ class FileApp(SQLModelBase, UUIDTableBaseMixin): is_restricted: bool = False """是否限制用户组访问""" - iframe_url_template: str | None = Field(default=None, max_length=1024) + iframe_url_template: Text1024 | None = None """iframe URL 模板,支持 {file_url} 占位符""" wopi_discovery_url: str | None = Field(default=None, max_length=512) """WOPI 客户端发现端点 URL""" - wopi_editor_url_template: str | None = Field(default=None, max_length=1024) + wopi_editor_url_template: Text1024 | None = None """WOPI 编辑器 URL 模板,支持 {wopi_src} {access_token} {access_token_ttl}""" # 关系 diff --git a/sqlmodels/group.py b/sqlmodels/group.py index 3b7f9a6..722d73f 100644 --- a/sqlmodels/group.py +++ b/sqlmodels/group.py @@ -5,7 +5,7 @@ from uuid import UUID from sqlalchemy import BigInteger from sqlmodel import Field, Relationship, text -from sqlmodel_ext import SQLModelBase, TableBaseMixin, UUIDTableBaseMixin +from sqlmodel_ext import SQLModelBase, TableBaseMixin, UUIDTableBaseMixin, Str255 if TYPE_CHECKING: from .user import User @@ -67,7 +67,7 @@ class GroupAllOptionsBase(GroupOptionsBase): class GroupCreateRequest(GroupAllOptionsBase): """创建用户组请求 DTO""" - name: str = Field(max_length=255) + name: Str255 """用户组名称""" max_storage: int = Field(default=0, ge=0) @@ -92,7 +92,7 @@ class GroupCreateRequest(GroupAllOptionsBase): class GroupUpdateRequest(SQLModelBase): """更新用户组请求 DTO(所有字段可选)""" - name: str | None = Field(default=None, max_length=255) + name: Str255 | None = None """用户组名称""" max_storage: int | None = Field(default=None, ge=0) @@ -258,7 +258,7 @@ class GroupOptions(GroupAllOptionsBase, TableBaseMixin): class Group(GroupBase, UUIDTableBaseMixin): """用户组模型""" - name: str = Field(max_length=255, unique=True) + name: Str255 = Field(unique=True) """用户组名""" max_storage: int = Field(default=0, sa_type=BigInteger, sa_column_kwargs={"server_default": "0"}) diff --git a/sqlmodels/migration.py b/sqlmodels/migration.py index a55bc9c..90b3223 100644 --- a/sqlmodels/migration.py +++ b/sqlmodels/migration.py @@ -130,6 +130,11 @@ default_settings: list[Setting] = [ Setting(name="sms_provider", value="", type=SettingsType.MOBILE), Setting(name="sms_access_key", value="", type=SettingsType.MOBILE), Setting(name="sms_secret_key", value="", type=SettingsType.MOBILE), + # ==================== 文件分类扩展名配置 ==================== + Setting(name="image", value="jpg,jpeg,png,gif,bmp,webp,svg,ico,tiff,tif,avif,heic,heif,psd,raw", type=SettingsType.FILE_CATEGORY), + Setting(name="video", value="mp4,mkv,avi,mov,wmv,flv,webm,m4v,ts,3gp,mpg,mpeg", type=SettingsType.FILE_CATEGORY), + Setting(name="audio", value="mp3,wav,flac,aac,ogg,wma,m4a,opus,ape,aiff,mid,midi", type=SettingsType.FILE_CATEGORY), + Setting(name="document", value="pdf,doc,docx,odt,rtf,txt,tex,epub,pages,ppt,pptx,odp,key,xls,xlsx,csv,ods,numbers,tsv,md,markdown,mdx", type=SettingsType.FILE_CATEGORY), ] async def init_default_settings() -> None: @@ -173,7 +178,7 @@ async def init_default_group() -> None: admin=True, ) admin_group_id = admin_group.id # 在 save 前保存 UUID - await admin_group.save(session) + admin_group = await admin_group.save(session) await GroupOptions( group_id=admin_group_id, @@ -203,7 +208,7 @@ async def init_default_group() -> None: web_dav_enabled=True, ) member_group_id = member_group.id # 在 save 前保存 UUID - await member_group.save(session) + member_group = await member_group.save(session) await GroupOptions( group_id=member_group_id, @@ -222,7 +227,7 @@ async def init_default_group() -> None: default_group_setting = await Setting.get(session, Setting.name == "default_group") if default_group_setting: default_group_setting.value = str(member_group_id) - await default_group_setting.save(session) + default_group_setting = await default_group_setting.save(session) # 未找到初始游客组时,则创建 if not await Group.get(session, Group.name == "游客"): @@ -232,7 +237,7 @@ async def init_default_group() -> None: web_dav_enabled=False, ) guest_group_id = guest_group.id # 在 save 前保存 UUID - await guest_group.save(session) + guest_group = await guest_group.save(session) await GroupOptions( group_id=guest_group_id, @@ -284,7 +289,7 @@ async def init_default_user() -> None: group_id=admin_group.id, ) admin_user_id = admin_user.id # 在 save 前保存 UUID - await admin_user.save(session) + admin_user = await admin_user.save(session) # 创建 AuthIdentity(邮箱密码身份) await AuthIdentity( @@ -373,7 +378,7 @@ async def init_default_theme_presets() -> None: error=ChromaticColor.RED, neutral=NeutralColor.ZINC, ) - await default_preset.save(session) + default_preset = await default_preset.save(session) log.info('已创建默认主题预设') @@ -522,6 +527,6 @@ async def init_default_file_apps() -> None: extension=ext.lower(), priority=i, ) - await ext_record.save(session) + ext_record = await ext_record.save(session) log.info(f'已创建 {len(_DEFAULT_FILE_APPS)} 个默认文件查看器应用') diff --git a/sqlmodels/node.py b/sqlmodels/node.py index 96fa787..092024d 100644 --- a/sqlmodels/node.py +++ b/sqlmodels/node.py @@ -3,7 +3,7 @@ from typing import TYPE_CHECKING from sqlmodel import Field, Relationship, text, Index -from sqlmodel_ext import SQLModelBase, TableBaseMixin +from sqlmodel_ext import SQLModelBase, TableBaseMixin, Str255 if TYPE_CHECKING: from .download import Download @@ -28,13 +28,13 @@ class NodeType(StrEnum): class Aria2ConfigurationBase(SQLModelBase): """Aria2配置基础模型""" - rpc_url: str | None = Field(default=None, max_length=255) + rpc_url: Str255 | None = None """RPC地址""" rpc_secret: str | None = None """RPC密钥""" - temp_path: str | None = Field(default=None, max_length=255) + temp_path: Str255 | None = None """临时下载路径""" max_concurrent: int = Field(default=5, ge=1, le=50) @@ -70,19 +70,19 @@ class Node(SQLModelBase, TableBaseMixin): status: NodeStatus = Field(default=NodeStatus.ONLINE) """节点状态""" - name: str = Field(max_length=255, unique=True) + name: Str255 = Field(unique=True) """节点名称""" type: NodeType """节点类型""" - server: str = Field(max_length=255) + server: Str255 """节点地址(IP或域名)""" - slave_key: str | None = Field(default=None, max_length=255) + slave_key: Str255 | None = None """从机通讯密钥""" - master_key: str | None = Field(default=None, max_length=255) + master_key: Str255 | None = None """主机通讯密钥""" aria2_enabled: bool = False diff --git a/sqlmodels/object.py b/sqlmodels/object.py index 2ce5480..3d04e1d 100644 --- a/sqlmodels/object.py +++ b/sqlmodels/object.py @@ -7,7 +7,7 @@ from enum import StrEnum from sqlalchemy import BigInteger from sqlmodel import Field, Relationship, CheckConstraint, Index, text -from sqlmodel_ext import SQLModelBase, UUIDTableBaseMixin +from sqlmodel_ext import SQLModelBase, UUIDTableBaseMixin, Str255, Str256 from .policy import PolicyType @@ -25,7 +25,15 @@ class ObjectType(StrEnum): """对象类型枚举""" FILE = "file" FOLDER = "folder" - + + +class FileCategory(StrEnum): + """文件类型分类枚举,用于按类别筛选文件""" + IMAGE = "image" + VIDEO = "video" + AUDIO = "audio" + DOCUMENT = "document" + # ==================== Base 模型 ==================== @@ -190,13 +198,13 @@ class Object(ObjectBase, UUIDTableBaseMixin): # ==================== 基础字段 ==================== - name: str = Field(max_length=255) + name: Str255 """对象名称(文件名或目录名)""" type: ObjectType """对象类型:file 或 folder""" - password: str | None = Field(default=None, max_length=255) + password: Str255 | None = None """对象独立密码(仅当用户为对象单独设置密码时有效)""" # ==================== 文件专属字段 ==================== @@ -204,7 +212,7 @@ class Object(ObjectBase, UUIDTableBaseMixin): size: int = Field(default=0, sa_type=BigInteger, sa_column_kwargs={"server_default": "0"}) """文件大小(字节),目录为 0""" - upload_session_id: str | None = Field(default=None, max_length=255, unique=True, index=True) + upload_session_id: Str255 | None = Field(default=None, unique=True, index=True) """分块上传会话ID(仅文件有效)""" physical_file_id: UUID | None = Field( @@ -469,6 +477,37 @@ class Object(ObjectBase, UUIDTableBaseMixin): fetch_mode="all" ) + @classmethod + async def get_by_category( + cls, + session: 'AsyncSession', + user_id: UUID, + extensions: list[str], + table_view: 'TableViewRequest | None' = None, + ) -> 'ListResponse[Object]': + """ + 按扩展名列表查询用户的所有文件(跨目录) + + 只查询未删除、未封禁的文件对象,使用 ILIKE 匹配文件名后缀。 + + :param session: 数据库会话 + :param user_id: 用户UUID + :param extensions: 扩展名列表(不含点号) + :param table_view: 分页排序参数 + :return: 分页文件列表 + """ + from sqlalchemy import or_ + + ext_conditions = [cls.name.ilike(f"%.{ext}") for ext in extensions] + condition = ( + (cls.owner_id == user_id) & + (cls.type == ObjectType.FILE) & + (cls.deleted_at == None) & + (cls.is_banned == False) & + or_(*ext_conditions) + ) + return await cls.get_with_count(session, condition, table_view=table_view) + @classmethod async def resolve_uri( cls, @@ -546,7 +585,7 @@ class Object(ObjectBase, UUIDTableBaseMixin): class UploadSessionBase(SQLModelBase): """上传会话基础字段""" - file_name: str = Field(max_length=255) + file_name: Str255 """原始文件名""" file_size: int = Field(ge=0, sa_type=BigInteger) @@ -577,7 +616,7 @@ class UploadSession(UploadSessionBase, UUIDTableBaseMixin): storage_path: str | None = Field(default=None, max_length=512) """文件存储路径""" - s3_upload_id: str | None = Field(default=None, max_length=256) + s3_upload_id: Str256 | None = None """S3 Multipart Upload ID(仅 S3 策略使用)""" s3_part_etags: str | None = None @@ -624,7 +663,7 @@ class UploadSession(UploadSessionBase, UUIDTableBaseMixin): class CreateUploadSessionRequest(SQLModelBase): """创建上传会话请求 DTO""" - file_name: str = Field(max_length=255) + file_name: Str255 """文件名""" file_size: int = Field(ge=0) @@ -681,7 +720,7 @@ class UploadChunkResponse(SQLModelBase): class CreateFileRequest(SQLModelBase): """创建空白文件请求 DTO""" - name: str = Field(max_length=255) + name: Str255 """文件名""" parent_id: UUID @@ -719,7 +758,7 @@ class ObjectRenameRequest(SQLModelBase): id: UUID """对象UUID""" - new_name: str = Field(max_length=255) + new_name: Str255 """新名称""" diff --git a/sqlmodels/object_metadata.py b/sqlmodels/object_metadata.py index e67d9c6..2441da6 100644 --- a/sqlmodels/object_metadata.py +++ b/sqlmodels/object_metadata.py @@ -25,7 +25,7 @@ from uuid import UUID from sqlmodel import Field, UniqueConstraint, Index, Relationship -from sqlmodel_ext import SQLModelBase, UUIDTableBaseMixin +from sqlmodel_ext import SQLModelBase, UUIDTableBaseMixin, Str255 if TYPE_CHECKING: from .object import Object @@ -65,7 +65,7 @@ USER_WRITABLE_NAMESPACES: set[str] = {MetadataNamespace.CUSTOM} class ObjectMetadataBase(SQLModelBase): """对象元数据 KV 基础模型""" - name: str = Field(max_length=255) + name: Str255 """元数据键名,格式:namespace:key(如 exif:width, stream:duration)""" value: str @@ -113,7 +113,7 @@ class MetadataResponse(SQLModelBase): class MetadataPatchItem(SQLModelBase): """单条元数据补丁 DTO""" - key: str = Field(max_length=255) + key: Str255 """元数据键名""" value: str | None = None diff --git a/sqlmodels/order.py b/sqlmodels/order.py index d93ff3e..7861f4b 100644 --- a/sqlmodels/order.py +++ b/sqlmodels/order.py @@ -1,58 +1,122 @@ +from decimal import Decimal from enum import StrEnum from typing import TYPE_CHECKING from uuid import UUID +from sqlalchemy import Numeric from sqlmodel import Field, Relationship -from sqlmodel_ext import SQLModelBase, TableBaseMixin +from sqlmodel_ext import SQLModelBase, TableBaseMixin, Str255 if TYPE_CHECKING: + from .product import Product from .user import User class OrderStatus(StrEnum): """订单状态枚举""" + PENDING = "pending" """待支付""" + COMPLETED = "completed" """已完成""" + CANCELLED = "cancelled" """已取消""" class OrderType(StrEnum): """订单类型枚举""" - # [TODO] 补充具体订单类型 - pass + STORAGE_PACK = "storage_pack" + """容量包""" + + GROUP_TIME = "group_time" + """用户组时长""" + + SCORE = "score" + """积分充值""" + + +# ==================== DTO 模型 ==================== + +class CreateOrderRequest(SQLModelBase): + """创建订单请求 DTO""" + + product_id: UUID + """商品UUID""" + + num: int = Field(default=1, ge=1) + """购买数量""" + + method: str + """支付方式""" + + +class OrderResponse(SQLModelBase): + """订单响应 DTO""" + + id: int + """订单ID""" + + order_no: str + """订单号""" + + type: OrderType + """订单类型""" + + method: str | None = None + """支付方式""" + + product_id: UUID | None = None + """商品UUID""" + + num: int + """购买数量""" + + name: str + """商品名称""" + + price: float + """订单价格(元)""" + + status: OrderStatus + """订单状态""" + + user_id: UUID + """所属用户UUID""" + + +# ==================== 数据库模型 ==================== class Order(SQLModelBase, TableBaseMixin): """订单模型""" - order_no: str = Field(max_length=255, unique=True, index=True) + order_no: Str255 = Field(unique=True, index=True) """订单号,唯一""" - type: int = Field(default=0, sa_column_kwargs={"server_default": "0"}) - """订单类型 [TODO] 待定义枚举""" + type: OrderType + """订单类型""" - method: str | None = Field(default=None, max_length=255) + method: Str255 | None = None """支付方式""" - product_id: int | None = Field(default=None) - """商品ID""" + product_id: UUID | None = Field(default=None, foreign_key="product.id", ondelete="SET NULL") + """关联商品UUID""" num: int = Field(default=1, sa_column_kwargs={"server_default": "1"}) """购买数量""" - name: str = Field(max_length=255) + name: Str255 """商品名称""" - price: int = Field(default=0, sa_column_kwargs={"server_default": "0"}) - """订单价格(分)""" + price: Decimal = Field(sa_type=Numeric(12, 2), default=Decimal("0.00")) + """订单价格(元)""" status: OrderStatus = Field(default=OrderStatus.PENDING) """订单状态""" - + # 外键 user_id: UUID = Field( foreign_key="user.id", @@ -60,6 +124,22 @@ class Order(SQLModelBase, TableBaseMixin): ondelete="CASCADE" ) """所属用户UUID""" - + # 关系 - user: "User" = Relationship(back_populates="orders") \ No newline at end of file + user: "User" = Relationship(back_populates="orders") + product: "Product" = Relationship(back_populates="orders") + + def to_response(self) -> OrderResponse: + """转换为响应 DTO""" + return OrderResponse( + id=self.id, + order_no=self.order_no, + type=self.type, + method=self.method, + product_id=self.product_id, + num=self.num, + name=self.name, + price=float(self.price), + status=self.status, + user_id=self.user_id, + ) diff --git a/sqlmodels/physical_file.py b/sqlmodels/physical_file.py index 72c8db4..b83c0bd 100644 --- a/sqlmodels/physical_file.py +++ b/sqlmodels/physical_file.py @@ -15,7 +15,7 @@ from uuid import UUID from sqlalchemy import BigInteger from sqlmodel import Field, Relationship, Index -from sqlmodel_ext import SQLModelBase, UUIDTableBaseMixin +from sqlmodel_ext import SQLModelBase, UUIDTableBaseMixin, Str32, Str64 if TYPE_CHECKING: from .object import Object @@ -31,10 +31,10 @@ class PhysicalFileBase(SQLModelBase): size: int = Field(default=0, sa_type=BigInteger) """文件大小(字节)""" - checksum_md5: str | None = Field(default=None, max_length=32) + checksum_md5: Str32 | None = None """MD5校验和(用于文件去重和完整性校验)""" - checksum_sha256: str | None = Field(default=None, max_length=64) + checksum_sha256: Str64 | None = None """SHA256校验和""" diff --git a/sqlmodels/policy.py b/sqlmodels/policy.py index 4d841a3..2083500 100644 --- a/sqlmodels/policy.py +++ b/sqlmodels/policy.py @@ -4,7 +4,7 @@ from uuid import UUID from enum import StrEnum from sqlmodel import Field, Relationship, text -from sqlmodel_ext import SQLModelBase, UUIDTableBaseMixin +from sqlmodel_ext import SQLModelBase, UUIDTableBaseMixin, Str64, Str255 if TYPE_CHECKING: from .object import Object @@ -37,22 +37,22 @@ class PolicyType(StrEnum): class PolicyBase(SQLModelBase): """存储策略基础字段,供 DTO 和数据库模型共享""" - name: str = Field(max_length=255) + name: Str255 """策略名称""" type: PolicyType """存储策略类型""" - server: str | None = Field(default=None, max_length=255) + server: Str255 | None = None """服务器地址(本地策略为绝对路径)""" - bucket_name: str | None = Field(default=None, max_length=255) + bucket_name: Str255 | None = None """存储桶名称""" is_private: bool = True """是否为私有空间""" - base_url: str | None = Field(default=None, max_length=255) + base_url: Str255 | None = None """访问文件的基础URL""" access_key: str | None = None @@ -67,10 +67,10 @@ class PolicyBase(SQLModelBase): auto_rename: bool = False """是否自动重命名""" - dir_name_rule: str | None = Field(default=None, max_length=255) + dir_name_rule: Str255 | None = None """目录命名规则""" - file_name_rule: str | None = Field(default=None, max_length=255) + file_name_rule: Str255 | None = None """文件命名规则""" is_origin_link_enable: bool = False @@ -115,7 +115,7 @@ class PolicyCreateRequest(PolicyBase): mimetype: str | None = Field(default=None, max_length=127) """MIME类型""" - od_redirect: str | None = Field(default=None, max_length=255) + od_redirect: Str255 | None = None """OneDrive重定向地址""" chunk_size: int = Field(default=52428800, ge=1) @@ -124,26 +124,26 @@ class PolicyCreateRequest(PolicyBase): s3_path_style: bool = False """是否使用S3路径风格""" - s3_region: str = Field(default='us-east-1', max_length=64) + s3_region: Str64 = 'us-east-1' """S3 区域(如 us-east-1、ap-southeast-1),仅 S3 策略使用""" class PolicyUpdateRequest(SQLModelBase): """更新存储策略请求 DTO(所有字段可选)""" - name: str | None = Field(default=None, max_length=255) + name: Str255 | None = None """策略名称""" - server: str | None = Field(default=None, max_length=255) + server: Str255 | None = None """服务器地址""" - bucket_name: str | None = Field(default=None, max_length=255) + bucket_name: Str255 | None = None """存储桶名称""" is_private: bool | None = None """是否为私有空间""" - base_url: str | None = Field(default=None, max_length=255) + base_url: Str255 | None = None """访问文件的基础URL""" access_key: str | None = None @@ -158,10 +158,10 @@ class PolicyUpdateRequest(SQLModelBase): auto_rename: bool | None = None """是否自动重命名""" - dir_name_rule: str | None = Field(default=None, max_length=255) + dir_name_rule: Str255 | None = None """目录命名规则""" - file_name_rule: str | None = Field(default=None, max_length=255) + file_name_rule: Str255 | None = None """文件命名规则""" is_origin_link_enable: bool | None = None @@ -177,7 +177,7 @@ class PolicyUpdateRequest(SQLModelBase): mimetype: str | None = Field(default=None, max_length=127) """MIME类型""" - od_redirect: str | None = Field(default=None, max_length=255) + od_redirect: Str255 | None = None """OneDrive重定向地址""" chunk_size: int | None = Field(default=None, ge=1) @@ -186,7 +186,7 @@ class PolicyUpdateRequest(SQLModelBase): s3_path_style: bool | None = None """是否使用S3路径风格""" - s3_region: str | None = Field(default=None, max_length=64) + s3_region: Str64 | None = None """S3 区域""" @@ -205,7 +205,7 @@ class PolicyOptionsBase(SQLModelBase): mimetype: str | None = Field(default=None, max_length=127) """MIME类型""" - od_redirect: str | None = Field(default=None, max_length=255) + od_redirect: Str255 | None = None """OneDrive重定向地址""" chunk_size: int = Field(default=52428800, sa_column_kwargs={"server_default": "52428800"}) @@ -214,7 +214,7 @@ class PolicyOptionsBase(SQLModelBase): s3_path_style: bool = Field(default=False, sa_column_kwargs={"server_default": text("false")}) """是否使用S3路径风格""" - s3_region: str = Field(default='us-east-1', max_length=64, sa_column_kwargs={"server_default": "'us-east-1'"}) + s3_region: Str64 = Field(default='us-east-1', sa_column_kwargs={"server_default": "'us-east-1'"}) """S3 区域(如 us-east-1、ap-southeast-1),仅 S3 策略使用""" @@ -237,7 +237,7 @@ class Policy(PolicyBase, UUIDTableBaseMixin): """存储策略模型""" # 覆盖基类字段以添加数据库专有配置 - name: str = Field(max_length=255, unique=True) + name: Str255 = Field(unique=True) """策略名称""" is_private: bool = Field(default=True, sa_column_kwargs={"server_default": text("true")}) diff --git a/sqlmodels/product.py b/sqlmodels/product.py new file mode 100644 index 0000000..188f947 --- /dev/null +++ b/sqlmodels/product.py @@ -0,0 +1,206 @@ +from decimal import Decimal +from enum import StrEnum +from typing import TYPE_CHECKING +from uuid import UUID + +from sqlalchemy import Numeric, BigInteger +from sqlmodel import Field, Relationship, text + +from sqlmodel_ext import SQLModelBase, UUIDTableBaseMixin, Str255 + +if TYPE_CHECKING: + from .order import Order + from .redeem import Redeem + + +class ProductType(StrEnum): + """商品类型枚举""" + + STORAGE_PACK = "storage_pack" + """容量包""" + + GROUP_TIME = "group_time" + """用户组时长""" + + SCORE = "score" + """积分充值""" + + +class PaymentMethod(StrEnum): + """支付方式枚举""" + + ALIPAY = "alipay" + """支付宝""" + + WECHAT = "wechat" + """微信支付""" + + STRIPE = "stripe" + """Stripe""" + + EASYPAY = "easypay" + """易支付""" + + CUSTOM = "custom" + """自定义支付""" + + +# ==================== DTO 模型 ==================== + +class ProductBase(SQLModelBase): + """商品基础字段""" + + name: str + """商品名称""" + + type: ProductType + """商品类型""" + + description: str | None = None + """商品描述""" + + +class ProductCreateRequest(ProductBase): + """创建商品请求 DTO""" + + name: Str255 + """商品名称""" + + price: Decimal = Field(ge=0, decimal_places=2) + """商品价格(元)""" + + is_active: bool = True + """是否上架""" + + sort_order: int = Field(default=0, ge=0) + """排序权重(越大越靠前)""" + + # storage_pack 专用 + size: int | None = Field(default=None, ge=0) + """容量大小(字节),type=storage_pack 时必填""" + + duration_days: int | None = Field(default=None, ge=1) + """有效天数,type=storage_pack/group_time 时必填""" + + # group_time 专用 + group_id: UUID | None = None + """目标用户组UUID,type=group_time 时必填""" + + # score 专用 + score_amount: int | None = Field(default=None, ge=1) + """积分数量,type=score 时必填""" + + +class ProductUpdateRequest(SQLModelBase): + """更新商品请求 DTO(所有字段可选)""" + + name: Str255 | None = None + """商品名称""" + + description: str | None = None + """商品描述""" + + price: Decimal | None = Field(default=None, ge=0, decimal_places=2) + """商品价格(元)""" + + is_active: bool | None = None + """是否上架""" + + sort_order: int | None = Field(default=None, ge=0) + """排序权重""" + + size: int | None = Field(default=None, ge=0) + """容量大小(字节)""" + + duration_days: int | None = Field(default=None, ge=1) + """有效天数""" + + group_id: UUID | None = None + """目标用户组UUID""" + + score_amount: int | None = Field(default=None, ge=1) + """积分数量""" + + +class ProductResponse(ProductBase): + """商品响应 DTO""" + + id: UUID + """商品UUID""" + + price: float + """商品价格(元)""" + + is_active: bool + """是否上架""" + + sort_order: int + """排序权重""" + + size: int | None = None + """容量大小(字节)""" + + duration_days: int | None = None + """有效天数""" + + group_id: UUID | None = None + """目标用户组UUID""" + + score_amount: int | None = None + """积分数量""" + + +# ==================== 数据库模型 ==================== + +class Product(ProductBase, UUIDTableBaseMixin): + """商品模型""" + + name: Str255 + """商品名称""" + + price: Decimal = Field(sa_type=Numeric(12, 2), default=Decimal("0.00")) + """商品价格(元)""" + + is_active: bool = Field(default=True, sa_column_kwargs={"server_default": text("true")}) + """是否上架""" + + sort_order: int = Field(default=0, sa_column_kwargs={"server_default": "0"}) + """排序权重(越大越靠前)""" + + # storage_pack 专用 + size: int | None = Field(default=None, sa_type=BigInteger) + """容量大小(字节),type=storage_pack 时必填""" + + duration_days: int | None = None + """有效天数,type=storage_pack/group_time 时必填""" + + # group_time 专用 + group_id: UUID | None = Field(default=None, foreign_key="group.id", ondelete="SET NULL") + """目标用户组UUID,type=group_time 时必填""" + + # score 专用 + score_amount: int | None = None + """积分数量,type=score 时必填""" + + # 关系 + orders: list["Order"] = Relationship(back_populates="product") + """关联的订单列表""" + + redeems: list["Redeem"] = Relationship(back_populates="product") + """关联的兑换码列表""" + + def to_response(self) -> ProductResponse: + """转换为响应 DTO""" + return ProductResponse( + id=self.id, + name=self.name, + type=self.type, + description=self.description, + price=float(self.price), + is_active=self.is_active, + sort_order=self.sort_order, + size=self.size, + duration_days=self.duration_days, + group_id=self.group_id, + score_amount=self.score_amount, + ) diff --git a/sqlmodels/redeem.py b/sqlmodels/redeem.py index e672b6c..9dc6eb3 100644 --- a/sqlmodels/redeem.py +++ b/sqlmodels/redeem.py @@ -1,22 +1,141 @@ +from datetime import datetime from enum import StrEnum +from typing import TYPE_CHECKING +from uuid import UUID -from sqlmodel import Field, text +from sqlmodel import Field, Relationship, text from sqlmodel_ext import SQLModelBase, TableBaseMixin +if TYPE_CHECKING: + from .product import Product + from .user import User + class RedeemType(StrEnum): """兑换码类型枚举""" - # [TODO] 补充具体兑换码类型 - pass + STORAGE_PACK = "storage_pack" + """容量包""" + + GROUP_TIME = "group_time" + """用户组时长""" + + SCORE = "score" + """积分充值""" + + +# ==================== DTO 模型 ==================== + +class RedeemCreateRequest(SQLModelBase): + """批量生成兑换码请求 DTO""" + + product_id: UUID + """关联商品UUID""" + + count: int = Field(default=1, ge=1, le=100) + """生成数量""" + + +class RedeemUseRequest(SQLModelBase): + """使用兑换码请求 DTO""" + + code: str + """兑换码""" + + +class RedeemInfoResponse(SQLModelBase): + """兑换码信息响应 DTO(用户侧)""" + + type: RedeemType + """兑换码类型""" + + product_name: str | None = None + """关联商品名称""" + + num: int + """可兑换数量""" + + is_used: bool + """是否已使用""" + + +class RedeemAdminResponse(SQLModelBase): + """兑换码管理响应 DTO(管理侧)""" + + id: int + """兑换码ID""" + + type: RedeemType + """兑换码类型""" + + product_id: UUID | None = None + """关联商品UUID""" + + num: int + """可兑换数量""" + + code: str + """兑换码""" + + is_used: bool + """是否已使用""" + + used_at: datetime | None = None + """使用时间""" + + used_by: UUID | None = None + """使用者UUID""" + + +# ==================== 数据库模型 ==================== class Redeem(SQLModelBase, TableBaseMixin): """兑换码模型""" - type: int = Field(default=0, sa_column_kwargs={"server_default": "0"}) - """兑换码类型 [TODO] 待定义枚举""" - product_id: int | None = Field(default=None, description="关联的商品/权益ID") - num: int = Field(default=1, sa_column_kwargs={"server_default": "1"}, description="可兑换数量/时长等") - code: str = Field(unique=True, index=True, description="兑换码,唯一") - used: bool = Field(default=False, sa_column_kwargs={"server_default": text("false")}, description="是否已使用") \ No newline at end of file + type: RedeemType + """兑换码类型""" + + product_id: UUID | None = Field(default=None, foreign_key="product.id", ondelete="SET NULL") + """关联商品UUID""" + + num: int = Field(default=1, sa_column_kwargs={"server_default": "1"}) + """可兑换数量/时长等""" + + code: str = Field(unique=True, index=True) + """兑换码,唯一""" + + is_used: bool = Field(default=False, sa_column_kwargs={"server_default": text("false")}) + """是否已使用""" + + used_at: datetime | None = None + """使用时间""" + + used_by: UUID | None = Field(default=None, foreign_key="user.id", ondelete="SET NULL") + """使用者UUID""" + + # 关系 + product: "Product" = Relationship(back_populates="redeems") + user: "User" = Relationship(back_populates="redeems") + + def to_admin_response(self) -> RedeemAdminResponse: + """转换为管理侧响应 DTO""" + return RedeemAdminResponse( + id=self.id, + type=self.type, + product_id=self.product_id, + num=self.num, + code=self.code, + is_used=self.is_used, + used_at=self.used_at, + used_by=self.used_by, + ) + + def to_info_response(self, product_name: str | None = None) -> RedeemInfoResponse: + """转换为用户侧响应 DTO""" + return RedeemInfoResponse( + type=self.type, + product_name=product_name, + num=self.num, + is_used=self.is_used, + ) diff --git a/sqlmodels/report.py b/sqlmodels/report.py index 885d720..ffee593 100644 --- a/sqlmodels/report.py +++ b/sqlmodels/report.py @@ -4,7 +4,7 @@ from uuid import UUID from sqlmodel import Field, Relationship -from sqlmodel_ext import SQLModelBase, TableBaseMixin +from sqlmodel_ext import SQLModelBase, TableBaseMixin, Str255 if TYPE_CHECKING: from .share import Share @@ -21,7 +21,7 @@ class Report(SQLModelBase, TableBaseMixin): reason: int = Field(default=0, sa_column_kwargs={"server_default": "0"}) """举报原因 [TODO] 待定义枚举""" - description: str | None = Field(default=None, max_length=255, description="补充描述") + description: Str255 | None = Field(default=None, description="补充描述") # 外键 share_id: UUID = Field( diff --git a/sqlmodels/setting.py b/sqlmodels/setting.py index 926549d..3060486 100644 --- a/sqlmodels/setting.py +++ b/sqlmodels/setting.py @@ -163,6 +163,7 @@ class SettingsType(StrEnum): VERSION = "version" VIEW = "view" WOPI = "wopi" + FILE_CATEGORY = "file_category" # 数据库模型 class Setting(SettingItem, TableBaseMixin): diff --git a/sqlmodels/share.py b/sqlmodels/share.py index 7045c67..46dce77 100644 --- a/sqlmodels/share.py +++ b/sqlmodels/share.py @@ -5,7 +5,7 @@ from uuid import UUID from sqlmodel import Field, Relationship, text, UniqueConstraint, Index -from sqlmodel_ext import SQLModelBase, UUIDTableBaseMixin +from sqlmodel_ext import SQLModelBase, UUIDTableBaseMixin, Str64, Str255 from .model_base import ResponseBase from .object import ObjectType @@ -52,10 +52,10 @@ class Share(SQLModelBase, UUIDTableBaseMixin): Index("ix_share_object", "object_id"), ) - code: str = Field(max_length=64, nullable=False, index=True) + code: Str64 = Field(nullable=False, index=True) """分享码""" - password: str | None = Field(default=None, max_length=255) + password: Str255 | None = None """分享密码(加密后)""" object_id: UUID = Field( @@ -80,7 +80,7 @@ class Share(SQLModelBase, UUIDTableBaseMixin): preview_enabled: bool = Field(default=True, sa_column_kwargs={"server_default": text("true")}) """是否允许预览""" - source_name: str | None = Field(default=None, max_length=255) + source_name: Str255 | None = None """源名称(冗余字段,便于展示)""" score: int = Field(default=0, ge=0) diff --git a/sqlmodels/source_link.py b/sqlmodels/source_link.py index e121ff9..1883662 100644 --- a/sqlmodels/source_link.py +++ b/sqlmodels/source_link.py @@ -4,7 +4,7 @@ from uuid import UUID from sqlmodel import Field, Relationship, Index -from sqlmodel_ext import SQLModelBase, TableBaseMixin +from sqlmodel_ext import SQLModelBase, TableBaseMixin, Str255 if TYPE_CHECKING: from .object import Object @@ -17,7 +17,7 @@ class SourceLink(SQLModelBase, TableBaseMixin): Index("ix_sourcelink_object_name", "object_id", "name"), ) - name: str = Field(max_length=255) + name: Str255 """链接名称""" downloads: int = Field(default=0, sa_column_kwargs={"server_default": "0"}) diff --git a/sqlmodels/storage_pack.py b/sqlmodels/storage_pack.py index c21830c..54d7ca8 100644 --- a/sqlmodels/storage_pack.py +++ b/sqlmodels/storage_pack.py @@ -1,23 +1,60 @@ - -from typing import TYPE_CHECKING from datetime import datetime +from typing import TYPE_CHECKING from uuid import UUID -from sqlmodel import Field, Relationship, Column, func, DateTime +from sqlalchemy import BigInteger +from sqlmodel import Field, Relationship -from sqlmodel_ext import SQLModelBase, TableBaseMixin +from sqlmodel_ext import SQLModelBase, TableBaseMixin, Str255 if TYPE_CHECKING: from .user import User + +# ==================== DTO 模型 ==================== + +class StoragePackResponse(SQLModelBase): + """容量包响应 DTO""" + + id: int + """容量包ID""" + + name: str + """容量包名称""" + + size: int + """容量大小(字节)""" + + active_time: datetime | None = None + """激活时间""" + + expired_time: datetime | None = None + """过期时间""" + + product_id: UUID | None = None + """来源商品UUID""" + + +# ==================== 数据库模型 ==================== + class StoragePack(SQLModelBase, TableBaseMixin): """容量包模型""" - name: str = Field(max_length=255, description="容量包名称") - active_time: datetime | None = Field(default=None, description="激活时间") - expired_time: datetime | None = Field(default=None, index=True, description="过期时间") - size: int = Field(description="容量包大小(字节)") - + name: Str255 + """容量包名称""" + + active_time: datetime | None = None + """激活时间""" + + expired_time: datetime | None = Field(default=None, index=True) + """过期时间""" + + size: int = Field(sa_type=BigInteger) + """容量包大小(字节)""" + + product_id: UUID | None = Field(default=None, foreign_key="product.id", ondelete="SET NULL") + """来源商品UUID""" + # 外键 user_id: UUID = Field( foreign_key="user.id", @@ -25,6 +62,17 @@ class StoragePack(SQLModelBase, TableBaseMixin): ondelete="CASCADE" ) """所属用户UUID""" - + # 关系 - user: "User" = Relationship(back_populates="storage_packs") \ No newline at end of file + user: "User" = Relationship(back_populates="storage_packs") + + def to_response(self) -> StoragePackResponse: + """转换为响应 DTO""" + return StoragePackResponse( + id=self.id, + name=self.name, + size=self.size, + active_time=self.active_time, + expired_time=self.expired_time, + product_id=self.product_id, + ) diff --git a/sqlmodels/tag.py b/sqlmodels/tag.py index b83e485..1dc3cd9 100644 --- a/sqlmodels/tag.py +++ b/sqlmodels/tag.py @@ -5,7 +5,7 @@ from datetime import datetime from sqlmodel import Field, Relationship, UniqueConstraint, Column, func, DateTime -from sqlmodel_ext import SQLModelBase, TableBaseMixin +from sqlmodel_ext import SQLModelBase, TableBaseMixin, Str255 if TYPE_CHECKING: from .user import User @@ -24,13 +24,13 @@ class Tag(SQLModelBase, TableBaseMixin): __table_args__ = (UniqueConstraint("name", "user_id", name="uq_tag_name_user"),) - name: str = Field(max_length=255) + name: Str255 """标签名称""" - icon: str | None = Field(default=None, max_length=255) + icon: Str255 | None = None """标签图标""" - color: str | None = Field(default=None, max_length=255) + color: Str255 | None = None """标签颜色""" type: TagType = Field(default=TagType.MANUAL) diff --git a/sqlmodels/theme_preset.py b/sqlmodels/theme_preset.py index 9d8475e..e814d64 100644 --- a/sqlmodels/theme_preset.py +++ b/sqlmodels/theme_preset.py @@ -3,7 +3,7 @@ from uuid import UUID from sqlmodel import Field -from sqlmodel_ext import SQLModelBase, UUIDTableBaseMixin +from sqlmodel_ext import SQLModelBase, UUIDTableBaseMixin, Str100 from .color import ChromaticColor, NeutralColor, ThemeColorsBase @@ -11,7 +11,7 @@ from .color import ChromaticColor, NeutralColor, ThemeColorsBase class ThemePresetBase(SQLModelBase): """主题预设基础字段""" - name: str = Field(max_length=100) + name: Str100 """预设名称""" is_default: bool = False @@ -42,7 +42,7 @@ class ThemePresetBase(SQLModelBase): class ThemePreset(ThemePresetBase, UUIDTableBaseMixin): """主题预设表""" - name: str = Field(max_length=100, unique=True) + name: Str100 = Field(unique=True) """预设名称(唯一约束)""" @@ -51,7 +51,7 @@ class ThemePreset(ThemePresetBase, UUIDTableBaseMixin): class ThemePresetCreateRequest(SQLModelBase): """创建主题预设请求 DTO""" - name: str = Field(max_length=100) + name: Str100 """预设名称""" colors: ThemeColorsBase @@ -61,7 +61,7 @@ class ThemePresetCreateRequest(SQLModelBase): class ThemePresetUpdateRequest(SQLModelBase): """更新主题预设请求 DTO""" - name: str | None = Field(default=None, max_length=100) + name: Str100 | None = None """预设名称(可选)""" colors: ThemeColorsBase | None = None diff --git a/sqlmodels/user.py b/sqlmodels/user.py index 9693a93..b15c94c 100644 --- a/sqlmodels/user.py +++ b/sqlmodels/user.py @@ -9,7 +9,7 @@ from sqlmodel import Field, Relationship from sqlmodel.ext.asyncio.session import AsyncSession from sqlmodel.main import RelationshipInfo -from sqlmodel_ext import SQLModelBase, UUIDTableBaseMixin, TableViewRequest, ListResponse +from sqlmodel_ext import SQLModelBase, UUIDTableBaseMixin, TableViewRequest, ListResponse, Str255 from .auth_identity import AuthProviderType from .color import ChromaticColor, NeutralColor, ThemeColorsBase @@ -23,6 +23,7 @@ if TYPE_CHECKING: from .download import Download from .object import Object from .order import Order + from .redeem import Redeem from .share import Share from .storage_pack import StoragePack from .tag import Tag @@ -476,7 +477,7 @@ class User(UserBase, UUIDTableBaseMixin): storage: int = Field(default=0, sa_type=BigInteger, sa_column_kwargs={"server_default": "0"}, ge=0) """已用存储空间(字节)""" - avatar: str = Field(default="default", max_length=255) + avatar: Str255 = Field(default="default") """头像地址""" score: int = Field(default=0, sa_column_kwargs={"server_default": "0"}, ge=0) @@ -570,6 +571,14 @@ class User(UserBase, UUIDTableBaseMixin): back_populates="user", sa_relationship_kwargs={"cascade": "all, delete-orphan"} ) + redeems: list["Redeem"] = Relationship( + back_populates="user", + sa_relationship_kwargs={ + "cascade": "all, delete-orphan", + "foreign_keys": "[Redeem.used_by]" + } + ) + """用户使用过的兑换码列表""" shares: list["Share"] = Relationship( back_populates="user", sa_relationship_kwargs={"cascade": "all, delete-orphan"} diff --git a/sqlmodels/user_authn.py b/sqlmodels/user_authn.py index 0997193..7997a1c 100644 --- a/sqlmodels/user_authn.py +++ b/sqlmodels/user_authn.py @@ -5,7 +5,7 @@ from uuid import UUID from sqlalchemy import Column, Text from sqlmodel import Field, Relationship -from sqlmodel_ext import SQLModelBase, TableBaseMixin +from sqlmodel_ext import SQLModelBase, TableBaseMixin, Str32, Str100, Str255 if TYPE_CHECKING: from .user import User @@ -51,7 +51,7 @@ class AuthnDetailResponse(SQLModelBase): class AuthnRenameRequest(SQLModelBase): """WebAuthn 凭证重命名请求 DTO""" - name: str = Field(max_length=100) + name: Str100 """新的凭证名称""" @@ -60,7 +60,7 @@ class AuthnRenameRequest(SQLModelBase): class UserAuthn(SQLModelBase, TableBaseMixin): """用户 WebAuthn 凭证模型,与 User 为多对一关系""" - credential_id: str = Field(max_length=255, unique=True, index=True) + credential_id: Str255 = Field(unique=True, index=True) """凭证 ID,Base64URL 编码""" credential_public_key: str = Field(sa_column=Column(Text)) @@ -69,16 +69,16 @@ class UserAuthn(SQLModelBase, TableBaseMixin): sign_count: int = Field(default=0, ge=0) """签名计数器,用于防重放攻击""" - credential_device_type: str = Field(max_length=32) + credential_device_type: Str32 """凭证设备类型:'single_device' 或 'multi_device'""" credential_backed_up: bool = Field(default=False) """凭证是否已备份""" - transports: str | None = Field(default=None, max_length=255) + transports: Str255 | None = None """支持的传输方式,逗号分隔,如 'usb,nfc,ble,internal'""" - name: str | None = Field(default=None, max_length=100) + name: Str100 | None = None """用户自定义的凭证名称,便于识别""" # 外键 diff --git a/sqlmodels/webdav.py b/sqlmodels/webdav.py index 7bdabcb..99e7964 100644 --- a/sqlmodels/webdav.py +++ b/sqlmodels/webdav.py @@ -9,7 +9,7 @@ from uuid import UUID from sqlmodel import Field, Relationship, UniqueConstraint -from sqlmodel_ext import SQLModelBase, TableBaseMixin +from sqlmodel_ext import SQLModelBase, TableBaseMixin, Str255 if TYPE_CHECKING: from .user import User @@ -20,7 +20,7 @@ if TYPE_CHECKING: class WebDAVBase(SQLModelBase): """WebDAV 账户基础字段""" - name: str = Field(max_length=255) + name: Str255 """账户名称(同一用户下唯一)""" root: str = Field(default="/", sa_column_kwargs={"server_default": "'/'"}) @@ -40,7 +40,7 @@ class WebDAV(WebDAVBase, TableBaseMixin): __table_args__ = (UniqueConstraint("name", "user_id", name="uq_webdav_name_user"),) - password: str = Field(max_length=255) + password: Str255 """密码(Argon2 哈希)""" # 外键 @@ -60,10 +60,10 @@ class WebDAV(WebDAVBase, TableBaseMixin): class WebDAVCreateRequest(SQLModelBase): """创建 WebDAV 账户请求""" - name: str = Field(max_length=255) + name: Str255 """账户名称""" - password: str = Field(min_length=1, max_length=255) + password: Str255 = Field(min_length=1) """账户密码(明文,服务端哈希后存储)""" root: str = "/" @@ -79,7 +79,7 @@ class WebDAVCreateRequest(SQLModelBase): class WebDAVUpdateRequest(SQLModelBase): """更新 WebDAV 账户请求""" - password: str | None = Field(default=None, min_length=1, max_length=255) + password: Str255 | None = Field(default=None, min_length=1) """新密码(为 None 时不修改)""" root: str | None = None diff --git a/tests/fixtures/users.py b/tests/fixtures/users.py index 0fefd57..8377ffe 100644 --- a/tests/fixtures/users.py +++ b/tests/fixtures/users.py @@ -71,7 +71,7 @@ class UserFactory: is_verified=True, user_id=user.id, ) - await identity.save(session) + identity = await identity.save(session) return user @@ -123,7 +123,7 @@ class UserFactory: is_verified=True, user_id=admin.id, ) - await identity.save(session) + identity = await identity.save(session) return admin @@ -170,7 +170,7 @@ class UserFactory: is_verified=True, user_id=banned_user.id, ) - await identity.save(session) + identity = await identity.save(session) return banned_user @@ -219,6 +219,6 @@ class UserFactory: is_verified=True, user_id=user.id, ) - await identity.save(session) + identity = await identity.save(session) return user diff --git a/uv.lock b/uv.lock index cab208f..5830aa7 100644 --- a/uv.lock +++ b/uv.lock @@ -238,11 +238,11 @@ wheels = [ [[package]] name = "cachetools" -version = "7.0.1" +version = "7.0.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d4/07/56595285564e90777d758ebd383d6b0b971b87729bbe2184a849932a3736/cachetools-7.0.1.tar.gz", hash = "sha256:e31e579d2c5b6e2944177a0397150d312888ddf4e16e12f1016068f0c03b8341", size = 36126, upload-time = "2026-02-10T22:24:05.03Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/cc/eb3fd22f3b96b8b70ce456d0854ef08434e5ca79c02bf8db3fc07ccfca87/cachetools-7.0.4.tar.gz", hash = "sha256:7042c0e4eea87812f04744ce6ee9ed3de457875eb1f82d8a206c46d6e48b6734", size = 37379, upload-time = "2026-03-08T21:37:17.133Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ed/9e/5faefbf9db1db466d633735faceda1f94aa99ce506ac450d232536266b32/cachetools-7.0.1-py3-none-any.whl", hash = "sha256:8f086515c254d5664ae2146d14fc7f65c9a4bce75152eb247e5a9c5e6d7b2ecf", size = 13484, upload-time = "2026-02-10T22:24:03.741Z" }, + { url = "https://files.pythonhosted.org/packages/83/bc/72adfb3f2ed19eb0317f89ea9b1eeccc670ae46bc394ec2c4ba1dd8c22b7/cachetools-7.0.4-py3-none-any.whl", hash = "sha256:0c8bb1b9ec8194fa4d764accfde602dfe52f70d0f311e62792d4c3f8c051b1e9", size = 13900, upload-time = "2026-03-08T21:37:15.805Z" }, ] [[package]] @@ -282,11 +282,11 @@ wheels = [ [[package]] name = "certifi" -version = "2026.1.4" +version = "2026.2.25" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e0/2d/a891ca51311197f6ad14a7ef42e2399f36cf2f9bd44752b3dc4eab60fdc5/certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120", size = 154268, upload-time = "2026-01-04T02:42:41.825Z" } +sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900, upload-time = "2026-01-04T02:42:40.15Z" }, + { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" }, ] [[package]] @@ -625,7 +625,7 @@ wheels = [ [[package]] name = "fastapi" -version = "0.132.0" +version = "0.135.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-doc" }, @@ -634,9 +634,9 @@ dependencies = [ { name = "typing-extensions" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a0/55/f1b4d4e478a0a1b4b1113d0f610a1b08e539b69900f97fdc97155d62fdee/fastapi-0.132.0.tar.gz", hash = "sha256:ef687847936d8a57ea6ea04cf9a85fe5f2c6ba64e22bfa721467094b69d48d92", size = 372422, upload-time = "2026-02-23T17:56:22.218Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/7b/f8e0211e9380f7195ba3f3d40c292594fd81ba8ec4629e3854c353aaca45/fastapi-0.135.1.tar.gz", hash = "sha256:d04115b508d936d254cea545b7312ecaa58a7b3a0f84952535b4c9afae7668cd", size = 394962, upload-time = "2026-03-01T18:18:29.369Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a8/de/6171c3363bbc5e01686e200e0880647c9270daa476d91030435cf14d32f5/fastapi-0.132.0-py3-none-any.whl", hash = "sha256:3c487d5afce196fa8ea509ae1531e96ccd5cdd2fd6eae78b73e2c20fba706689", size = 104652, upload-time = "2026-02-23T17:56:20.836Z" }, + { url = "https://files.pythonhosted.org/packages/e4/72/42e900510195b23a56bde950d26a51f8b723846bfcaa0286e90287f0422b/fastapi-0.135.1-py3-none-any.whl", hash = "sha256:46e2fc5745924b7c840f71ddd277382af29ce1cdb7d5eab5bf697e3fb9999c9e", size = 116999, upload-time = "2026-03-01T18:18:30.831Z" }, ] [package.optional-dependencies] @@ -653,16 +653,16 @@ standard = [ [[package]] name = "fastapi-cli" -version = "0.0.23" +version = "0.0.24" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "rich-toolkit" }, { name = "typer" }, { name = "uvicorn", extra = ["standard"] }, ] -sdist = { url = "https://files.pythonhosted.org/packages/71/9f/cbd463e57de4e977b8ea0403f95347f9150441568b1d3fe3e4949ef80ef3/fastapi_cli-0.0.23.tar.gz", hash = "sha256:210ac280ea41e73aac5a57688781256beb23c2cba3a41266896fa43e6445c8e7", size = 19763, upload-time = "2026-02-16T19:45:53.358Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6e/58/74797ae9e4610cfa0c6b34c8309096d3b20bb29be3b8b5fbf1004d10fa5f/fastapi_cli-0.0.24.tar.gz", hash = "sha256:1afc9c9e21d7ebc8a3ca5e31790cd8d837742be7e4f8b9236e99cb3451f0de00", size = 19043, upload-time = "2026-02-24T10:45:10.476Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/68/89/19dcfd5cd289b306abdcabac68b88a4f54b7710a2c33adc16a337ecdcdfa/fastapi_cli-0.0.23-py3-none-any.whl", hash = "sha256:7e9634fc212da0b6cfc75bd3ac366cc9dfdb43b5e9ec12e58bfd1acdd2697f25", size = 12305, upload-time = "2026-02-16T19:45:52.554Z" }, + { url = "https://files.pythonhosted.org/packages/c7/4b/68f9fe268e535d79c76910519530026a4f994ce07189ac0dded45c6af825/fastapi_cli-0.0.24-py3-none-any.whl", hash = "sha256:4a1f78ed798f106b4fee85ca93b85d8fe33c0a3570f775964d37edb80b8f0edc", size = 12304, upload-time = "2026-02-24T10:45:09.552Z" }, ] [package.optional-dependencies] @@ -673,7 +673,7 @@ standard = [ [[package]] name = "fastapi-cloud-cli" -version = "0.13.0" +version = "0.14.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "fastar" }, @@ -685,9 +685,9 @@ dependencies = [ { name = "typer" }, { name = "uvicorn", extra = ["standard"] }, ] -sdist = { url = "https://files.pythonhosted.org/packages/de/0b/f07f4976784978ef159fd2e8f5c16f1f9d610578fb1fd976ff1315c11ea6/fastapi_cloud_cli-0.13.0.tar.gz", hash = "sha256:4d8f42337e8021c648f6cb0672de7d5b31b0fc7387a83d7b12f974600ac3f2fd", size = 38436, upload-time = "2026-02-17T05:18:19.033Z" } +sdist = { url = "https://files.pythonhosted.org/packages/74/30/1665ad6bd1c285d1c6947e6ab0eae168bc44a9b45d5fc11fa930603db1c7/fastapi_cloud_cli-0.14.1.tar.gz", hash = "sha256:5b086182570008f67d9ae989870102f595e4e216493cabcd270ef6cd0401339f", size = 39999, upload-time = "2026-03-08T01:40:24.166Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b4/88/71a1e989d17b9edb483f32e28b7891ffdd3005271518c98ba6415987c430/fastapi_cloud_cli-0.13.0-py3-none-any.whl", hash = "sha256:874a9ed8dba34ec828f198c72de9f9a38de77ac1b15083d6bc3a4d772b0bc477", size = 27631, upload-time = "2026-02-17T05:18:18.094Z" }, + { url = "https://files.pythonhosted.org/packages/b6/c2/0117d2a1b93eb7c6d2084e6be320d34a404f621eb01a26c1471c0eb4ee82/fastapi_cloud_cli-0.14.1-py3-none-any.whl", hash = "sha256:99ab3a2fbd1880121a62fb9c4f584e15c42ef0fe762cb5f7380a548081e60d05", size = 28359, upload-time = "2026-03-08T01:40:24.949Z" }, ] [[package]] @@ -1630,11 +1630,11 @@ wheels = [ [[package]] name = "python-dotenv" -version = "1.2.1" +version = "1.2.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f0/26/19cadc79a718c5edbec86fd4919a6b6d3f681039a2f6d66d14be94e75fb9/python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6", size = 44221, upload-time = "2025-10-26T15:12:10.434Z" } +sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" }, + { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, ] [[package]] @@ -1684,11 +1684,11 @@ wheels = [ [[package]] name = "redis" -version = "7.2.0" +version = "7.3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9f/32/6fac13a11e73e1bc67a2ae821a72bfe4c2d8c4c48f0267e4a952be0f1bae/redis-7.2.0.tar.gz", hash = "sha256:4dd5bf4bd4ae80510267f14185a15cba2a38666b941aff68cccf0256b51c1f26", size = 4901247, upload-time = "2026-02-16T17:16:22.797Z" } +sdist = { url = "https://files.pythonhosted.org/packages/da/82/4d1a5279f6c1251d3d2a603a798a1137c657de9b12cfc1fba4858232c4d2/redis-7.3.0.tar.gz", hash = "sha256:4d1b768aafcf41b01022410b3cc4f15a07d9b3d6fe0c66fc967da2c88e551034", size = 4928081, upload-time = "2026-03-06T18:18:16.287Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/86/cf/f6180b67f99688d83e15c84c5beda831d1d341e95872d224f87ccafafe61/redis-7.2.0-py3-none-any.whl", hash = "sha256:01f591f8598e483f1842d429e8ae3a820804566f1c73dca1b80e23af9fba0497", size = 394898, upload-time = "2026-02-16T17:16:20.693Z" }, + { url = "https://files.pythonhosted.org/packages/f0/28/84e57fce7819e81ec5aa1bd31c42b89607241f4fb1a3ea5b0d2dbeaea26c/redis-7.3.0-py3-none-any.whl", hash = "sha256:9d4fcb002a12a5e3c3fbe005d59c48a2cc231f87fbb2f6b70c2d89bb64fec364", size = 404379, upload-time = "2026-03-06T18:18:14.583Z" }, ] [package.optional-dependencies] @@ -1711,16 +1711,16 @@ wheels = [ [[package]] name = "rich-toolkit" -version = "0.19.4" +version = "0.19.7" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "rich" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d0/c9/4bbf4bfee195ed1b7d7a6733cc523ca61dbfb4a3e3c12ea090aaffd97597/rich_toolkit-0.19.4.tar.gz", hash = "sha256:52e23d56f9dc30d1343eb3b3f6f18764c313fbfea24e52e6a1d6069bec9c18eb", size = 193951, upload-time = "2026-02-12T10:08:15.814Z" } +sdist = { url = "https://files.pythonhosted.org/packages/42/ba/dae9e3096651042754da419a4042bc1c75e07d615f9b15066d738838e4df/rich_toolkit-0.19.7.tar.gz", hash = "sha256:133c0915872da91d4c25d85342d5ec1dfacc69b63448af1a08a0d4b4f23ef46e", size = 195877, upload-time = "2026-02-24T16:06:20.555Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/28/31/97d39719def09c134385bfcfbedfed255168b571e7beb3ad7765aae660ca/rich_toolkit-0.19.4-py3-none-any.whl", hash = "sha256:34ac344de8862801644be8b703e26becf44b047e687f208d7829e8f7cfc311d6", size = 32757, upload-time = "2026-02-12T10:08:15.037Z" }, + { url = "https://files.pythonhosted.org/packages/fb/3c/c923619f6d2f5fafcc96fec0aaf9550a46cd5b6481f06e0c6b66a2a4fed0/rich_toolkit-0.19.7-py3-none-any.whl", hash = "sha256:0288e9203728c47c5a4eb60fd2f0692d9df7455a65901ab6f898437a2ba5989d", size = 32963, upload-time = "2026-02-24T16:06:22.066Z" }, ] [[package]] @@ -1778,15 +1778,15 @@ wheels = [ [[package]] name = "sentry-sdk" -version = "2.53.0" +version = "2.54.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d3/06/66c8b705179bc54087845f28fd1b72f83751b6e9a195628e2e9af9926505/sentry_sdk-2.53.0.tar.gz", hash = "sha256:6520ef2c4acd823f28efc55e43eb6ce2e6d9f954a95a3aa96b6fd14871e92b77", size = 412369, upload-time = "2026-02-16T11:11:14.743Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c8/e9/2e3a46c304e7fa21eaa70612f60354e32699c7102eb961f67448e222ad7c/sentry_sdk-2.54.0.tar.gz", hash = "sha256:2620c2575128d009b11b20f7feb81e4e4e8ae08ec1d36cbc845705060b45cc1b", size = 413813, upload-time = "2026-03-02T15:12:41.355Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/47/d4/2fdf854bc3b9c7f55219678f812600a20a138af2dd847d99004994eada8f/sentry_sdk-2.53.0-py2.py3-none-any.whl", hash = "sha256:46e1ed8d84355ae54406c924f6b290c3d61f4048625989a723fd622aab838899", size = 437908, upload-time = "2026-02-16T11:11:13.227Z" }, + { url = "https://files.pythonhosted.org/packages/53/39/be412cc86bc6247b8f69e9383d7950711bd86f8d0a4a4b0fe8fad685bc21/sentry_sdk-2.54.0-py2.py3-none-any.whl", hash = "sha256:fd74e0e281dcda63afff095d23ebcd6e97006102cdc8e78a29f19ecdf796a0de", size = 439198, upload-time = "2026-03-02T15:12:39.546Z" }, ] [[package]] @@ -1809,37 +1809,41 @@ wheels = [ [[package]] name = "sqlalchemy" -version = "2.0.46" +version = "2.0.48" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/06/aa/9ce0f3e7a9829ead5c8ce549392f33a12c4555a6c0609bb27d882e9c7ddf/sqlalchemy-2.0.46.tar.gz", hash = "sha256:cf36851ee7219c170bb0793dbc3da3e80c582e04a5437bc601bfe8c85c9216d7", size = 9865393, upload-time = "2026-01-21T18:03:45.119Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1f/73/b4a9737255583b5fa858e0bb8e116eb94b88c910164ed2ed719147bde3de/sqlalchemy-2.0.48.tar.gz", hash = "sha256:5ca74f37f3369b45e1f6b7b06afb182af1fd5dde009e4ffd831830d98cbe5fe7", size = 9886075, upload-time = "2026-03-02T15:28:51.474Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b3/4b/fa7838fe20bb752810feed60e45625a9a8b0102c0c09971e2d1d95362992/sqlalchemy-2.0.46-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:93a12da97cca70cea10d4b4fc602589c4511f96c1f8f6c11817620c021d21d00", size = 2150268, upload-time = "2026-01-21T19:05:56.621Z" }, - { url = "https://files.pythonhosted.org/packages/46/c1/b34dccd712e8ea846edf396e00973dda82d598cb93762e55e43e6835eba9/sqlalchemy-2.0.46-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:af865c18752d416798dae13f83f38927c52f085c52e2f32b8ab0fef46fdd02c2", size = 3276511, upload-time = "2026-01-21T18:46:49.022Z" }, - { url = "https://files.pythonhosted.org/packages/96/48/a04d9c94753e5d5d096c628c82a98c4793b9c08ca0e7155c3eb7d7db9f24/sqlalchemy-2.0.46-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8d679b5f318423eacb61f933a9a0f75535bfca7056daeadbf6bd5bcee6183aee", size = 3292881, upload-time = "2026-01-21T18:40:13.089Z" }, - { url = "https://files.pythonhosted.org/packages/be/f4/06eda6e91476f90a7d8058f74311cb65a2fb68d988171aced81707189131/sqlalchemy-2.0.46-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:64901e08c33462acc9ec3bad27fc7a5c2b6491665f2aa57564e57a4f5d7c52ad", size = 3224559, upload-time = "2026-01-21T18:46:50.974Z" }, - { url = "https://files.pythonhosted.org/packages/ab/a2/d2af04095412ca6345ac22b33b89fe8d6f32a481e613ffcb2377d931d8d0/sqlalchemy-2.0.46-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e8ac45e8f4eaac0f9f8043ea0e224158855c6a4329fd4ee37c45c61e3beb518e", size = 3262728, upload-time = "2026-01-21T18:40:14.883Z" }, - { url = "https://files.pythonhosted.org/packages/31/48/1980c7caa5978a3b8225b4d230e69a2a6538a3562b8b31cea679b6933c83/sqlalchemy-2.0.46-cp313-cp313-win32.whl", hash = "sha256:8d3b44b3d0ab2f1319d71d9863d76eeb46766f8cf9e921ac293511804d39813f", size = 2111295, upload-time = "2026-01-21T18:42:52.366Z" }, - { url = "https://files.pythonhosted.org/packages/2d/54/f8d65bbde3d877617c4720f3c9f60e99bb7266df0d5d78b6e25e7c149f35/sqlalchemy-2.0.46-cp313-cp313-win_amd64.whl", hash = "sha256:77f8071d8fbcbb2dd11b7fd40dedd04e8ebe2eb80497916efedba844298065ef", size = 2137076, upload-time = "2026-01-21T18:42:53.924Z" }, - { url = "https://files.pythonhosted.org/packages/56/ba/9be4f97c7eb2b9d5544f2624adfc2853e796ed51d2bb8aec90bc94b7137e/sqlalchemy-2.0.46-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a1e8cc6cc01da346dc92d9509a63033b9b1bda4fed7a7a7807ed385c7dccdc10", size = 3556533, upload-time = "2026-01-21T18:33:06.636Z" }, - { url = "https://files.pythonhosted.org/packages/20/a6/b1fc6634564dbb4415b7ed6419cdfeaadefd2c39cdab1e3aa07a5f2474c2/sqlalchemy-2.0.46-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:96c7cca1a4babaaf3bfff3e4e606e38578856917e52f0384635a95b226c87764", size = 3523208, upload-time = "2026-01-21T18:45:08.436Z" }, - { url = "https://files.pythonhosted.org/packages/a1/d8/41e0bdfc0f930ff236f86fccd12962d8fa03713f17ed57332d38af6a3782/sqlalchemy-2.0.46-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b2a9f9aee38039cf4755891a1e50e1effcc42ea6ba053743f452c372c3152b1b", size = 3464292, upload-time = "2026-01-21T18:33:08.208Z" }, - { url = "https://files.pythonhosted.org/packages/f0/8b/9dcbec62d95bea85f5ecad9b8d65b78cc30fb0ffceeb3597961f3712549b/sqlalchemy-2.0.46-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:db23b1bf8cfe1f7fda19018e7207b20cdb5168f83c437ff7e95d19e39289c447", size = 3473497, upload-time = "2026-01-21T18:45:10.552Z" }, - { url = "https://files.pythonhosted.org/packages/e9/f8/5ecdfc73383ec496de038ed1614de9e740a82db9ad67e6e4514ebc0708a3/sqlalchemy-2.0.46-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:56bdd261bfd0895452006d5316cbf35739c53b9bb71a170a331fa0ea560b2ada", size = 2152079, upload-time = "2026-01-21T19:05:58.477Z" }, - { url = "https://files.pythonhosted.org/packages/e5/bf/eba3036be7663ce4d9c050bc3d63794dc29fbe01691f2bf5ccb64e048d20/sqlalchemy-2.0.46-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:33e462154edb9493f6c3ad2125931e273bbd0be8ae53f3ecd1c161ea9a1dd366", size = 3272216, upload-time = "2026-01-21T18:46:52.634Z" }, - { url = "https://files.pythonhosted.org/packages/05/45/1256fb597bb83b58a01ddb600c59fe6fdf0e5afe333f0456ed75c0f8d7bd/sqlalchemy-2.0.46-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9bcdce05f056622a632f1d44bb47dbdb677f58cad393612280406ce37530eb6d", size = 3277208, upload-time = "2026-01-21T18:40:16.38Z" }, - { url = "https://files.pythonhosted.org/packages/d9/a0/2053b39e4e63b5d7ceb3372cface0859a067c1ddbd575ea7e9985716f771/sqlalchemy-2.0.46-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8e84b09a9b0f19accedcbeff5c2caf36e0dd537341a33aad8d680336152dc34e", size = 3221994, upload-time = "2026-01-21T18:46:54.622Z" }, - { url = "https://files.pythonhosted.org/packages/1e/87/97713497d9502553c68f105a1cb62786ba1ee91dea3852ae4067ed956a50/sqlalchemy-2.0.46-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4f52f7291a92381e9b4de9050b0a65ce5d6a763333406861e33906b8aa4906bf", size = 3243990, upload-time = "2026-01-21T18:40:18.253Z" }, - { url = "https://files.pythonhosted.org/packages/a8/87/5d1b23548f420ff823c236f8bea36b1a997250fd2f892e44a3838ca424f4/sqlalchemy-2.0.46-cp314-cp314-win32.whl", hash = "sha256:70ed2830b169a9960193f4d4322d22be5c0925357d82cbf485b3369893350908", size = 2114215, upload-time = "2026-01-21T18:42:55.232Z" }, - { url = "https://files.pythonhosted.org/packages/3a/20/555f39cbcf0c10cf452988b6a93c2a12495035f68b3dbd1a408531049d31/sqlalchemy-2.0.46-cp314-cp314-win_amd64.whl", hash = "sha256:3c32e993bc57be6d177f7d5d31edb93f30726d798ad86ff9066d75d9bf2e0b6b", size = 2139867, upload-time = "2026-01-21T18:42:56.474Z" }, - { url = "https://files.pythonhosted.org/packages/3e/f0/f96c8057c982d9d8a7a68f45d69c674bc6f78cad401099692fe16521640a/sqlalchemy-2.0.46-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4dafb537740eef640c4d6a7c254611dca2df87eaf6d14d6a5fca9d1f4c3fc0fa", size = 3561202, upload-time = "2026-01-21T18:33:10.337Z" }, - { url = "https://files.pythonhosted.org/packages/d7/53/3b37dda0a5b137f21ef608d8dfc77b08477bab0fe2ac9d3e0a66eaeab6fc/sqlalchemy-2.0.46-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:42a1643dc5427b69aca967dae540a90b0fbf57eaf248f13a90ea5930e0966863", size = 3526296, upload-time = "2026-01-21T18:45:12.657Z" }, - { url = "https://files.pythonhosted.org/packages/33/75/f28622ba6dde79cd545055ea7bd4062dc934e0621f7b3be2891f8563f8de/sqlalchemy-2.0.46-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ff33c6e6ad006bbc0f34f5faf941cfc62c45841c64c0a058ac38c799f15b5ede", size = 3470008, upload-time = "2026-01-21T18:33:11.725Z" }, - { url = "https://files.pythonhosted.org/packages/a9/42/4afecbbc38d5e99b18acef446453c76eec6fbd03db0a457a12a056836e22/sqlalchemy-2.0.46-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:82ec52100ec1e6ec671563bbd02d7c7c8d0b9e71a0723c72f22ecf52d1755330", size = 3476137, upload-time = "2026-01-21T18:45:15.001Z" }, - { url = "https://files.pythonhosted.org/packages/fc/a1/9c4efa03300926601c19c18582531b45aededfb961ab3c3585f1e24f120b/sqlalchemy-2.0.46-py3-none-any.whl", hash = "sha256:f9c11766e7e7c0a2767dda5acb006a118640c9fc0a4104214b96269bfb78399e", size = 1937882, upload-time = "2026-01-21T18:22:10.456Z" }, + { url = "https://files.pythonhosted.org/packages/d1/c6/569dc8bf3cd375abc5907e82235923e986799f301cd79a903f784b996fca/sqlalchemy-2.0.48-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e3070c03701037aa418b55d36532ecb8f8446ed0135acb71c678dbdf12f5b6e4", size = 2152599, upload-time = "2026-03-02T15:49:14.41Z" }, + { url = "https://files.pythonhosted.org/packages/6d/ff/f4e04a4bd5a24304f38cb0d4aa2ad4c0fb34999f8b884c656535e1b2b74c/sqlalchemy-2.0.48-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2645b7d8a738763b664a12a1542c89c940daa55196e8d73e55b169cc5c99f65f", size = 3278825, upload-time = "2026-03-02T15:50:38.269Z" }, + { url = "https://files.pythonhosted.org/packages/fe/88/cb59509e4668d8001818d7355d9995be90c321313078c912420603a7cb95/sqlalchemy-2.0.48-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b19151e76620a412c2ac1c6f977ab1b9fa7ad43140178345136456d5265b32ed", size = 3295200, upload-time = "2026-03-02T15:53:29.366Z" }, + { url = "https://files.pythonhosted.org/packages/87/dc/1609a4442aefd750ea2f32629559394ec92e89ac1d621a7f462b70f736ff/sqlalchemy-2.0.48-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5b193a7e29fd9fa56e502920dca47dffe60f97c863494946bd698c6058a55658", size = 3226876, upload-time = "2026-03-02T15:50:39.802Z" }, + { url = "https://files.pythonhosted.org/packages/37/c3/6ae2ab5ea2fa989fbac4e674de01224b7a9d744becaf59bb967d62e99bed/sqlalchemy-2.0.48-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:36ac4ddc3d33e852da9cb00ffb08cea62ca05c39711dc67062ca2bb1fae35fd8", size = 3265045, upload-time = "2026-03-02T15:53:31.421Z" }, + { url = "https://files.pythonhosted.org/packages/6f/82/ea4665d1bb98c50c19666e672f21b81356bd6077c4574e3d2bbb84541f53/sqlalchemy-2.0.48-cp313-cp313-win32.whl", hash = "sha256:389b984139278f97757ea9b08993e7b9d1142912e046ab7d82b3fbaeb0209131", size = 2113700, upload-time = "2026-03-02T15:54:35.825Z" }, + { url = "https://files.pythonhosted.org/packages/b7/2b/b9040bec58c58225f073f5b0c1870defe1940835549dafec680cbd58c3c3/sqlalchemy-2.0.48-cp313-cp313-win_amd64.whl", hash = "sha256:d612c976cbc2d17edfcc4c006874b764e85e990c29ce9bd411f926bbfb02b9a2", size = 2139487, upload-time = "2026-03-02T15:54:37.079Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/7b17bd50244b78a49d22cc63c969d71dc4de54567dc152a9b46f6fae40ce/sqlalchemy-2.0.48-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69f5bc24904d3bc3640961cddd2523e361257ef68585d6e364166dfbe8c78fae", size = 3558851, upload-time = "2026-03-02T15:57:48.607Z" }, + { url = "https://files.pythonhosted.org/packages/20/0d/213668e9aca61d370f7d2a6449ea4ec699747fac67d4bda1bb3d129025be/sqlalchemy-2.0.48-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd08b90d211c086181caed76931ecfa2bdfc83eea3cfccdb0f82abc6c4b876cb", size = 3525525, upload-time = "2026-03-02T16:04:38.058Z" }, + { url = "https://files.pythonhosted.org/packages/85/d7/a84edf412979e7d59c69b89a5871f90a49228360594680e667cb2c46a828/sqlalchemy-2.0.48-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:1ccd42229aaac2df431562117ac7e667d702e8e44afdb6cf0e50fa3f18160f0b", size = 3466611, upload-time = "2026-03-02T15:57:50.759Z" }, + { url = "https://files.pythonhosted.org/packages/86/55/42404ce5770f6be26a2b0607e7866c31b9a4176c819e9a7a5e0a055770be/sqlalchemy-2.0.48-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f0dcbc588cd5b725162c076eb9119342f6579c7f7f55057bb7e3c6ff27e13121", size = 3475812, upload-time = "2026-03-02T16:04:40.092Z" }, + { url = "https://files.pythonhosted.org/packages/ae/ae/29b87775fadc43e627cf582fe3bda4d02e300f6b8f2747c764950d13784c/sqlalchemy-2.0.48-cp313-cp313t-win32.whl", hash = "sha256:9764014ef5e58aab76220c5664abb5d47d5bc858d9debf821e55cfdd0f128485", size = 2141335, upload-time = "2026-03-02T15:52:51.518Z" }, + { url = "https://files.pythonhosted.org/packages/91/44/f39d063c90f2443e5b46ec4819abd3d8de653893aae92df42a5c4f5843de/sqlalchemy-2.0.48-cp313-cp313t-win_amd64.whl", hash = "sha256:e2f35b4cccd9ed286ad62e0a3c3ac21e06c02abc60e20aa51a3e305a30f5fa79", size = 2173095, upload-time = "2026-03-02T15:52:52.79Z" }, + { url = "https://files.pythonhosted.org/packages/f7/b3/f437eaa1cf028bb3c927172c7272366393e73ccd104dcf5b6963f4ab5318/sqlalchemy-2.0.48-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e2d0d88686e3d35a76f3e15a34e8c12d73fc94c1dea1cd55782e695cc14086dd", size = 2154401, upload-time = "2026-03-02T15:49:17.24Z" }, + { url = "https://files.pythonhosted.org/packages/6c/1c/b3abdf0f402aa3f60f0df6ea53d92a162b458fca2321d8f1f00278506402/sqlalchemy-2.0.48-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49b7bddc1eebf011ea5ab722fdbe67a401caa34a350d278cc7733c0e88fecb1f", size = 3274528, upload-time = "2026-03-02T15:50:41.489Z" }, + { url = "https://files.pythonhosted.org/packages/f2/5e/327428a034407651a048f5e624361adf3f9fbac9d0fa98e981e9c6ff2f5e/sqlalchemy-2.0.48-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:426c5ca86415d9b8945c7073597e10de9644802e2ff502b8e1f11a7a2642856b", size = 3279523, upload-time = "2026-03-02T15:53:32.962Z" }, + { url = "https://files.pythonhosted.org/packages/2a/ca/ece73c81a918add0965b76b868b7b5359e068380b90ef1656ee995940c02/sqlalchemy-2.0.48-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:288937433bd44e3990e7da2402fabc44a3c6c25d3704da066b85b89a85474ae0", size = 3224312, upload-time = "2026-03-02T15:50:42.996Z" }, + { url = "https://files.pythonhosted.org/packages/88/11/fbaf1ae91fa4ee43f4fe79661cead6358644824419c26adb004941bdce7c/sqlalchemy-2.0.48-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8183dc57ae7d9edc1346e007e840a9f3d6aa7b7f165203a99e16f447150140d2", size = 3246304, upload-time = "2026-03-02T15:53:34.937Z" }, + { url = "https://files.pythonhosted.org/packages/fa/a8/5fb0deb13930b4f2f698c5541ae076c18981173e27dd00376dbaea7a9c82/sqlalchemy-2.0.48-cp314-cp314-win32.whl", hash = "sha256:1182437cb2d97988cfea04cf6cdc0b0bb9c74f4d56ec3d08b81e23d621a28cc6", size = 2116565, upload-time = "2026-03-02T15:54:38.321Z" }, + { url = "https://files.pythonhosted.org/packages/95/7e/e83615cb63f80047f18e61e31e8e32257d39458426c23006deeaf48f463b/sqlalchemy-2.0.48-cp314-cp314-win_amd64.whl", hash = "sha256:144921da96c08feb9e2b052c5c5c1d0d151a292c6135623c6b2c041f2a45f9e0", size = 2142205, upload-time = "2026-03-02T15:54:39.831Z" }, + { url = "https://files.pythonhosted.org/packages/83/e3/69d8711b3f2c5135e9cde5f063bc1605860f0b2c53086d40c04017eb1f77/sqlalchemy-2.0.48-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5aee45fd2c6c0f2b9cdddf48c48535e7471e42d6fb81adfde801da0bd5b93241", size = 3563519, upload-time = "2026-03-02T15:57:52.387Z" }, + { url = "https://files.pythonhosted.org/packages/f8/4f/a7cce98facca73c149ea4578981594aaa5fd841e956834931de503359336/sqlalchemy-2.0.48-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7cddca31edf8b0653090cbb54562ca027c421c58ddde2c0685f49ff56a1690e0", size = 3528611, upload-time = "2026-03-02T16:04:42.097Z" }, + { url = "https://files.pythonhosted.org/packages/cd/7d/5936c7a03a0b0cb0fa0cc425998821c6029756b0855a8f7ee70fba1de955/sqlalchemy-2.0.48-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7a936f1bb23d370b7c8cc079d5fce4c7d18da87a33c6744e51a93b0f9e97e9b3", size = 3472326, upload-time = "2026-03-02T15:57:54.423Z" }, + { url = "https://files.pythonhosted.org/packages/f4/33/cea7dfc31b52904efe3dcdc169eb4514078887dff1f5ae28a7f4c5d54b3c/sqlalchemy-2.0.48-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e004aa9248e8cb0a5f9b96d003ca7c1c0a5da8decd1066e7b53f59eb8ce7c62b", size = 3478453, upload-time = "2026-03-02T16:04:44.584Z" }, + { url = "https://files.pythonhosted.org/packages/c8/95/32107c4d13be077a9cae61e9ae49966a35dc4bf442a8852dd871db31f62e/sqlalchemy-2.0.48-cp314-cp314t-win32.whl", hash = "sha256:b8438ec5594980d405251451c5b7ea9aa58dda38eb7ac35fb7e4c696712ee24f", size = 2147209, upload-time = "2026-03-02T15:52:54.274Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d7/1e073da7a4bc645eb83c76067284a0374e643bc4be57f14cc6414656f92c/sqlalchemy-2.0.48-cp314-cp314t-win_amd64.whl", hash = "sha256:d854b3970067297f3a7fbd7a4683587134aa9b3877ee15aa29eea478dc68f933", size = 2182198, upload-time = "2026-03-02T15:52:55.606Z" }, + { url = "https://files.pythonhosted.org/packages/46/2c/9664130905f03db57961b8980b05cab624afd114bf2be2576628a9f22da4/sqlalchemy-2.0.48-py3-none-any.whl", hash = "sha256:a66fe406437dd65cacd96a72689a3aaaecaebbcd62d81c5ac1c0fdbeac835096", size = 1940202, upload-time = "2026-03-02T15:52:43.285Z" }, ] [[package]] @@ -2126,78 +2130,86 @@ wheels = [ [[package]] name = "yarl" -version = "1.22.0" +version = "1.23.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "idna" }, { name = "multidict" }, { name = "propcache" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/57/63/0c6ebca57330cd313f6102b16dd57ffaf3ec4c83403dcb45dbd15c6f3ea1/yarl-1.22.0.tar.gz", hash = "sha256:bebf8557577d4401ba8bd9ff33906f1376c877aa78d1fe216ad01b4d6745af71", size = 187169, upload-time = "2025-10-06T14:12:55.963Z" } +sdist = { url = "https://files.pythonhosted.org/packages/23/6e/beb1beec874a72f23815c1434518bfc4ed2175065173fb138c3705f658d4/yarl-1.23.0.tar.gz", hash = "sha256:53b1ea6ca88ebd4420379c330aea57e258408dd0df9af0992e5de2078dc9f5d5", size = 194676, upload-time = "2026-03-01T22:07:53.373Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ea/f3/d67de7260456ee105dc1d162d43a019ecad6b91e2f51809d6cddaa56690e/yarl-1.22.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8dee9c25c74997f6a750cd317b8ca63545169c098faee42c84aa5e506c819b53", size = 139980, upload-time = "2025-10-06T14:10:14.601Z" }, - { url = "https://files.pythonhosted.org/packages/01/88/04d98af0b47e0ef42597b9b28863b9060bb515524da0a65d5f4db160b2d5/yarl-1.22.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:01e73b85a5434f89fc4fe27dcda2aff08ddf35e4d47bbbea3bdcd25321af538a", size = 93424, upload-time = "2025-10-06T14:10:16.115Z" }, - { url = "https://files.pythonhosted.org/packages/18/91/3274b215fd8442a03975ce6bee5fe6aa57a8326b29b9d3d56234a1dca244/yarl-1.22.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:22965c2af250d20c873cdbee8ff958fb809940aeb2e74ba5f20aaf6b7ac8c70c", size = 93821, upload-time = "2025-10-06T14:10:17.993Z" }, - { url = "https://files.pythonhosted.org/packages/61/3a/caf4e25036db0f2da4ca22a353dfeb3c9d3c95d2761ebe9b14df8fc16eb0/yarl-1.22.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4f15793aa49793ec8d1c708ab7f9eded1aa72edc5174cae703651555ed1b601", size = 373243, upload-time = "2025-10-06T14:10:19.44Z" }, - { url = "https://files.pythonhosted.org/packages/6e/9e/51a77ac7516e8e7803b06e01f74e78649c24ee1021eca3d6a739cb6ea49c/yarl-1.22.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5542339dcf2747135c5c85f68680353d5cb9ffd741c0f2e8d832d054d41f35a", size = 342361, upload-time = "2025-10-06T14:10:21.124Z" }, - { url = "https://files.pythonhosted.org/packages/d4/f8/33b92454789dde8407f156c00303e9a891f1f51a0330b0fad7c909f87692/yarl-1.22.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5c401e05ad47a75869c3ab3e35137f8468b846770587e70d71e11de797d113df", size = 387036, upload-time = "2025-10-06T14:10:22.902Z" }, - { url = "https://files.pythonhosted.org/packages/d9/9a/c5db84ea024f76838220280f732970aa4ee154015d7f5c1bfb60a267af6f/yarl-1.22.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:243dda95d901c733f5b59214d28b0120893d91777cb8aa043e6ef059d3cddfe2", size = 397671, upload-time = "2025-10-06T14:10:24.523Z" }, - { url = "https://files.pythonhosted.org/packages/11/c9/cd8538dc2e7727095e0c1d867bad1e40c98f37763e6d995c1939f5fdc7b1/yarl-1.22.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bec03d0d388060058f5d291a813f21c011041938a441c593374da6077fe21b1b", size = 377059, upload-time = "2025-10-06T14:10:26.406Z" }, - { url = "https://files.pythonhosted.org/packages/a1/b9/ab437b261702ced75122ed78a876a6dec0a1b0f5e17a4ac7a9a2482d8abe/yarl-1.22.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b0748275abb8c1e1e09301ee3cf90c8a99678a4e92e4373705f2a2570d581273", size = 365356, upload-time = "2025-10-06T14:10:28.461Z" }, - { url = "https://files.pythonhosted.org/packages/b2/9d/8e1ae6d1d008a9567877b08f0ce4077a29974c04c062dabdb923ed98e6fe/yarl-1.22.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:47fdb18187e2a4e18fda2c25c05d8251a9e4a521edaed757fef033e7d8498d9a", size = 361331, upload-time = "2025-10-06T14:10:30.541Z" }, - { url = "https://files.pythonhosted.org/packages/ca/5a/09b7be3905962f145b73beb468cdd53db8aa171cf18c80400a54c5b82846/yarl-1.22.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c7044802eec4524fde550afc28edda0dd5784c4c45f0be151a2d3ba017daca7d", size = 382590, upload-time = "2025-10-06T14:10:33.352Z" }, - { url = "https://files.pythonhosted.org/packages/aa/7f/59ec509abf90eda5048b0bc3e2d7b5099dffdb3e6b127019895ab9d5ef44/yarl-1.22.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:139718f35149ff544caba20fce6e8a2f71f1e39b92c700d8438a0b1d2a631a02", size = 385316, upload-time = "2025-10-06T14:10:35.034Z" }, - { url = "https://files.pythonhosted.org/packages/e5/84/891158426bc8036bfdfd862fabd0e0fa25df4176ec793e447f4b85cf1be4/yarl-1.22.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e1b51bebd221006d3d2f95fbe124b22b247136647ae5dcc8c7acafba66e5ee67", size = 374431, upload-time = "2025-10-06T14:10:37.76Z" }, - { url = "https://files.pythonhosted.org/packages/bb/49/03da1580665baa8bef5e8ed34c6df2c2aca0a2f28bf397ed238cc1bbc6f2/yarl-1.22.0-cp313-cp313-win32.whl", hash = "sha256:d3e32536234a95f513bd374e93d717cf6b2231a791758de6c509e3653f234c95", size = 81555, upload-time = "2025-10-06T14:10:39.649Z" }, - { url = "https://files.pythonhosted.org/packages/9a/ee/450914ae11b419eadd067c6183ae08381cfdfcb9798b90b2b713bbebddda/yarl-1.22.0-cp313-cp313-win_amd64.whl", hash = "sha256:47743b82b76d89a1d20b83e60d5c20314cbd5ba2befc9cda8f28300c4a08ed4d", size = 86965, upload-time = "2025-10-06T14:10:41.313Z" }, - { url = "https://files.pythonhosted.org/packages/98/4d/264a01eae03b6cf629ad69bae94e3b0e5344741e929073678e84bf7a3e3b/yarl-1.22.0-cp313-cp313-win_arm64.whl", hash = "sha256:5d0fcda9608875f7d052eff120c7a5da474a6796fe4d83e152e0e4d42f6d1a9b", size = 81205, upload-time = "2025-10-06T14:10:43.167Z" }, - { url = "https://files.pythonhosted.org/packages/88/fc/6908f062a2f77b5f9f6d69cecb1747260831ff206adcbc5b510aff88df91/yarl-1.22.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:719ae08b6972befcba4310e49edb1161a88cdd331e3a694b84466bd938a6ab10", size = 146209, upload-time = "2025-10-06T14:10:44.643Z" }, - { url = "https://files.pythonhosted.org/packages/65/47/76594ae8eab26210b4867be6f49129861ad33da1f1ebdf7051e98492bf62/yarl-1.22.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:47d8a5c446df1c4db9d21b49619ffdba90e77c89ec6e283f453856c74b50b9e3", size = 95966, upload-time = "2025-10-06T14:10:46.554Z" }, - { url = "https://files.pythonhosted.org/packages/ab/ce/05e9828a49271ba6b5b038b15b3934e996980dd78abdfeb52a04cfb9467e/yarl-1.22.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cfebc0ac8333520d2d0423cbbe43ae43c8838862ddb898f5ca68565e395516e9", size = 97312, upload-time = "2025-10-06T14:10:48.007Z" }, - { url = "https://files.pythonhosted.org/packages/d1/c5/7dffad5e4f2265b29c9d7ec869c369e4223166e4f9206fc2243ee9eea727/yarl-1.22.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4398557cbf484207df000309235979c79c4356518fd5c99158c7d38203c4da4f", size = 361967, upload-time = "2025-10-06T14:10:49.997Z" }, - { url = "https://files.pythonhosted.org/packages/50/b2/375b933c93a54bff7fc041e1a6ad2c0f6f733ffb0c6e642ce56ee3b39970/yarl-1.22.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2ca6fd72a8cd803be290d42f2dec5cdcd5299eeb93c2d929bf060ad9efaf5de0", size = 323949, upload-time = "2025-10-06T14:10:52.004Z" }, - { url = "https://files.pythonhosted.org/packages/66/50/bfc2a29a1d78644c5a7220ce2f304f38248dc94124a326794e677634b6cf/yarl-1.22.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca1f59c4e1ab6e72f0a23c13fca5430f889634166be85dbf1013683e49e3278e", size = 361818, upload-time = "2025-10-06T14:10:54.078Z" }, - { url = "https://files.pythonhosted.org/packages/46/96/f3941a46af7d5d0f0498f86d71275696800ddcdd20426298e572b19b91ff/yarl-1.22.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c5010a52015e7c70f86eb967db0f37f3c8bd503a695a49f8d45700144667708", size = 372626, upload-time = "2025-10-06T14:10:55.767Z" }, - { url = "https://files.pythonhosted.org/packages/c1/42/8b27c83bb875cd89448e42cd627e0fb971fa1675c9ec546393d18826cb50/yarl-1.22.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d7672ecf7557476642c88497c2f8d8542f8e36596e928e9bcba0e42e1e7d71f", size = 341129, upload-time = "2025-10-06T14:10:57.985Z" }, - { url = "https://files.pythonhosted.org/packages/49/36/99ca3122201b382a3cf7cc937b95235b0ac944f7e9f2d5331d50821ed352/yarl-1.22.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:3b7c88eeef021579d600e50363e0b6ee4f7f6f728cd3486b9d0f3ee7b946398d", size = 346776, upload-time = "2025-10-06T14:10:59.633Z" }, - { url = "https://files.pythonhosted.org/packages/85/b4/47328bf996acd01a4c16ef9dcd2f59c969f495073616586f78cd5f2efb99/yarl-1.22.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f4afb5c34f2c6fecdcc182dfcfc6af6cccf1aa923eed4d6a12e9d96904e1a0d8", size = 334879, upload-time = "2025-10-06T14:11:01.454Z" }, - { url = "https://files.pythonhosted.org/packages/c2/ad/b77d7b3f14a4283bffb8e92c6026496f6de49751c2f97d4352242bba3990/yarl-1.22.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:59c189e3e99a59cf8d83cbb31d4db02d66cda5a1a4374e8a012b51255341abf5", size = 350996, upload-time = "2025-10-06T14:11:03.452Z" }, - { url = "https://files.pythonhosted.org/packages/81/c8/06e1d69295792ba54d556f06686cbd6a7ce39c22307100e3fb4a2c0b0a1d/yarl-1.22.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:5a3bf7f62a289fa90f1990422dc8dff5a458469ea71d1624585ec3a4c8d6960f", size = 356047, upload-time = "2025-10-06T14:11:05.115Z" }, - { url = "https://files.pythonhosted.org/packages/4b/b8/4c0e9e9f597074b208d18cef227d83aac36184bfbc6eab204ea55783dbc5/yarl-1.22.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:de6b9a04c606978fdfe72666fa216ffcf2d1a9f6a381058d4378f8d7b1e5de62", size = 342947, upload-time = "2025-10-06T14:11:08.137Z" }, - { url = "https://files.pythonhosted.org/packages/e0/e5/11f140a58bf4c6ad7aca69a892bff0ee638c31bea4206748fc0df4ebcb3a/yarl-1.22.0-cp313-cp313t-win32.whl", hash = "sha256:1834bb90991cc2999f10f97f5f01317f99b143284766d197e43cd5b45eb18d03", size = 86943, upload-time = "2025-10-06T14:11:10.284Z" }, - { url = "https://files.pythonhosted.org/packages/31/74/8b74bae38ed7fe6793d0c15a0c8207bbb819cf287788459e5ed230996cdd/yarl-1.22.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ff86011bd159a9d2dfc89c34cfd8aff12875980e3bd6a39ff097887520e60249", size = 93715, upload-time = "2025-10-06T14:11:11.739Z" }, - { url = "https://files.pythonhosted.org/packages/69/66/991858aa4b5892d57aef7ee1ba6b4d01ec3b7eb3060795d34090a3ca3278/yarl-1.22.0-cp313-cp313t-win_arm64.whl", hash = "sha256:7861058d0582b847bc4e3a4a4c46828a410bca738673f35a29ba3ca5db0b473b", size = 83857, upload-time = "2025-10-06T14:11:13.586Z" }, - { url = "https://files.pythonhosted.org/packages/46/b3/e20ef504049f1a1c54a814b4b9bed96d1ac0e0610c3b4da178f87209db05/yarl-1.22.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:34b36c2c57124530884d89d50ed2c1478697ad7473efd59cfd479945c95650e4", size = 140520, upload-time = "2025-10-06T14:11:15.465Z" }, - { url = "https://files.pythonhosted.org/packages/e4/04/3532d990fdbab02e5ede063676b5c4260e7f3abea2151099c2aa745acc4c/yarl-1.22.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:0dd9a702591ca2e543631c2a017e4a547e38a5c0f29eece37d9097e04a7ac683", size = 93504, upload-time = "2025-10-06T14:11:17.106Z" }, - { url = "https://files.pythonhosted.org/packages/11/63/ff458113c5c2dac9a9719ac68ee7c947cb621432bcf28c9972b1c0e83938/yarl-1.22.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:594fcab1032e2d2cc3321bb2e51271e7cd2b516c7d9aee780ece81b07ff8244b", size = 94282, upload-time = "2025-10-06T14:11:19.064Z" }, - { url = "https://files.pythonhosted.org/packages/a7/bc/315a56aca762d44a6aaaf7ad253f04d996cb6b27bad34410f82d76ea8038/yarl-1.22.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f3d7a87a78d46a2e3d5b72587ac14b4c16952dd0887dbb051451eceac774411e", size = 372080, upload-time = "2025-10-06T14:11:20.996Z" }, - { url = "https://files.pythonhosted.org/packages/3f/3f/08e9b826ec2e099ea6e7c69a61272f4f6da62cb5b1b63590bb80ca2e4a40/yarl-1.22.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:852863707010316c973162e703bddabec35e8757e67fcb8ad58829de1ebc8590", size = 338696, upload-time = "2025-10-06T14:11:22.847Z" }, - { url = "https://files.pythonhosted.org/packages/e3/9f/90360108e3b32bd76789088e99538febfea24a102380ae73827f62073543/yarl-1.22.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:131a085a53bfe839a477c0845acf21efc77457ba2bcf5899618136d64f3303a2", size = 387121, upload-time = "2025-10-06T14:11:24.889Z" }, - { url = "https://files.pythonhosted.org/packages/98/92/ab8d4657bd5b46a38094cfaea498f18bb70ce6b63508fd7e909bd1f93066/yarl-1.22.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:078a8aefd263f4d4f923a9677b942b445a2be970ca24548a8102689a3a8ab8da", size = 394080, upload-time = "2025-10-06T14:11:27.307Z" }, - { url = "https://files.pythonhosted.org/packages/f5/e7/d8c5a7752fef68205296201f8ec2bf718f5c805a7a7e9880576c67600658/yarl-1.22.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bca03b91c323036913993ff5c738d0842fc9c60c4648e5c8d98331526df89784", size = 372661, upload-time = "2025-10-06T14:11:29.387Z" }, - { url = "https://files.pythonhosted.org/packages/b6/2e/f4d26183c8db0bb82d491b072f3127fb8c381a6206a3a56332714b79b751/yarl-1.22.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:68986a61557d37bb90d3051a45b91fa3d5c516d177dfc6dd6f2f436a07ff2b6b", size = 364645, upload-time = "2025-10-06T14:11:31.423Z" }, - { url = "https://files.pythonhosted.org/packages/80/7c/428e5812e6b87cd00ee8e898328a62c95825bf37c7fa87f0b6bb2ad31304/yarl-1.22.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:4792b262d585ff0dff6bcb787f8492e40698443ec982a3568c2096433660c694", size = 355361, upload-time = "2025-10-06T14:11:33.055Z" }, - { url = "https://files.pythonhosted.org/packages/ec/2a/249405fd26776f8b13c067378ef4d7dd49c9098d1b6457cdd152a99e96a9/yarl-1.22.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:ebd4549b108d732dba1d4ace67614b9545b21ece30937a63a65dd34efa19732d", size = 381451, upload-time = "2025-10-06T14:11:35.136Z" }, - { url = "https://files.pythonhosted.org/packages/67/a8/fb6b1adbe98cf1e2dd9fad71003d3a63a1bc22459c6e15f5714eb9323b93/yarl-1.22.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f87ac53513d22240c7d59203f25cc3beac1e574c6cd681bbfd321987b69f95fd", size = 383814, upload-time = "2025-10-06T14:11:37.094Z" }, - { url = "https://files.pythonhosted.org/packages/d9/f9/3aa2c0e480fb73e872ae2814c43bc1e734740bb0d54e8cb2a95925f98131/yarl-1.22.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:22b029f2881599e2f1b06f8f1db2ee63bd309e2293ba2d566e008ba12778b8da", size = 370799, upload-time = "2025-10-06T14:11:38.83Z" }, - { url = "https://files.pythonhosted.org/packages/50/3c/af9dba3b8b5eeb302f36f16f92791f3ea62e3f47763406abf6d5a4a3333b/yarl-1.22.0-cp314-cp314-win32.whl", hash = "sha256:6a635ea45ba4ea8238463b4f7d0e721bad669f80878b7bfd1f89266e2ae63da2", size = 82990, upload-time = "2025-10-06T14:11:40.624Z" }, - { url = "https://files.pythonhosted.org/packages/ac/30/ac3a0c5bdc1d6efd1b41fa24d4897a4329b3b1e98de9449679dd327af4f0/yarl-1.22.0-cp314-cp314-win_amd64.whl", hash = "sha256:0d6e6885777af0f110b0e5d7e5dda8b704efed3894da26220b7f3d887b839a79", size = 88292, upload-time = "2025-10-06T14:11:42.578Z" }, - { url = "https://files.pythonhosted.org/packages/df/0a/227ab4ff5b998a1b7410abc7b46c9b7a26b0ca9e86c34ba4b8d8bc7c63d5/yarl-1.22.0-cp314-cp314-win_arm64.whl", hash = "sha256:8218f4e98d3c10d683584cb40f0424f4b9fd6e95610232dd75e13743b070ee33", size = 82888, upload-time = "2025-10-06T14:11:44.863Z" }, - { url = "https://files.pythonhosted.org/packages/06/5e/a15eb13db90abd87dfbefb9760c0f3f257ac42a5cac7e75dbc23bed97a9f/yarl-1.22.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:45c2842ff0e0d1b35a6bf1cd6c690939dacb617a70827f715232b2e0494d55d1", size = 146223, upload-time = "2025-10-06T14:11:46.796Z" }, - { url = "https://files.pythonhosted.org/packages/18/82/9665c61910d4d84f41a5bf6837597c89e665fa88aa4941080704645932a9/yarl-1.22.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d947071e6ebcf2e2bee8fce76e10faca8f7a14808ca36a910263acaacef08eca", size = 95981, upload-time = "2025-10-06T14:11:48.845Z" }, - { url = "https://files.pythonhosted.org/packages/5d/9a/2f65743589809af4d0a6d3aa749343c4b5f4c380cc24a8e94a3c6625a808/yarl-1.22.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:334b8721303e61b00019474cc103bdac3d7b1f65e91f0bfedeec2d56dfe74b53", size = 97303, upload-time = "2025-10-06T14:11:50.897Z" }, - { url = "https://files.pythonhosted.org/packages/b0/ab/5b13d3e157505c43c3b43b5a776cbf7b24a02bc4cccc40314771197e3508/yarl-1.22.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e7ce67c34138a058fd092f67d07a72b8e31ff0c9236e751957465a24b28910c", size = 361820, upload-time = "2025-10-06T14:11:52.549Z" }, - { url = "https://files.pythonhosted.org/packages/fb/76/242a5ef4677615cf95330cfc1b4610e78184400699bdda0acb897ef5e49a/yarl-1.22.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d77e1b2c6d04711478cb1c4ab90db07f1609ccf06a287d5607fcd90dc9863acf", size = 323203, upload-time = "2025-10-06T14:11:54.225Z" }, - { url = "https://files.pythonhosted.org/packages/8c/96/475509110d3f0153b43d06164cf4195c64d16999e0c7e2d8a099adcd6907/yarl-1.22.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4647674b6150d2cae088fc07de2738a84b8bcedebef29802cf0b0a82ab6face", size = 363173, upload-time = "2025-10-06T14:11:56.069Z" }, - { url = "https://files.pythonhosted.org/packages/c9/66/59db471aecfbd559a1fd48aedd954435558cd98c7d0da8b03cc6c140a32c/yarl-1.22.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:efb07073be061c8f79d03d04139a80ba33cbd390ca8f0297aae9cce6411e4c6b", size = 373562, upload-time = "2025-10-06T14:11:58.783Z" }, - { url = "https://files.pythonhosted.org/packages/03/1f/c5d94abc91557384719da10ff166b916107c1b45e4d0423a88457071dd88/yarl-1.22.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e51ac5435758ba97ad69617e13233da53908beccc6cfcd6c34bbed8dcbede486", size = 339828, upload-time = "2025-10-06T14:12:00.686Z" }, - { url = "https://files.pythonhosted.org/packages/5f/97/aa6a143d3afba17b6465733681c70cf175af89f76ec8d9286e08437a7454/yarl-1.22.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:33e32a0dd0c8205efa8e83d04fc9f19313772b78522d1bdc7d9aed706bfd6138", size = 347551, upload-time = "2025-10-06T14:12:02.628Z" }, - { url = "https://files.pythonhosted.org/packages/43/3c/45a2b6d80195959239a7b2a8810506d4eea5487dce61c2a3393e7fc3c52e/yarl-1.22.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:bf4a21e58b9cde0e401e683ebd00f6ed30a06d14e93f7c8fd059f8b6e8f87b6a", size = 334512, upload-time = "2025-10-06T14:12:04.871Z" }, - { url = "https://files.pythonhosted.org/packages/86/a0/c2ab48d74599c7c84cb104ebd799c5813de252bea0f360ffc29d270c2caa/yarl-1.22.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:e4b582bab49ac33c8deb97e058cd67c2c50dac0dd134874106d9c774fd272529", size = 352400, upload-time = "2025-10-06T14:12:06.624Z" }, - { url = "https://files.pythonhosted.org/packages/32/75/f8919b2eafc929567d3d8411f72bdb1a2109c01caaab4ebfa5f8ffadc15b/yarl-1.22.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0b5bcc1a9c4839e7e30b7b30dd47fe5e7e44fb7054ec29b5bb8d526aa1041093", size = 357140, upload-time = "2025-10-06T14:12:08.362Z" }, - { url = "https://files.pythonhosted.org/packages/cf/72/6a85bba382f22cf78add705d8c3731748397d986e197e53ecc7835e76de7/yarl-1.22.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c0232bce2170103ec23c454e54a57008a9a72b5d1c3105dc2496750da8cfa47c", size = 341473, upload-time = "2025-10-06T14:12:10.994Z" }, - { url = "https://files.pythonhosted.org/packages/35/18/55e6011f7c044dc80b98893060773cefcfdbf60dfefb8cb2f58b9bacbd83/yarl-1.22.0-cp314-cp314t-win32.whl", hash = "sha256:8009b3173bcd637be650922ac455946197d858b3630b6d8787aa9e5c4564533e", size = 89056, upload-time = "2025-10-06T14:12:13.317Z" }, - { url = "https://files.pythonhosted.org/packages/f9/86/0f0dccb6e59a9e7f122c5afd43568b1d31b8ab7dda5f1b01fb5c7025c9a9/yarl-1.22.0-cp314-cp314t-win_amd64.whl", hash = "sha256:9fb17ea16e972c63d25d4a97f016d235c78dd2344820eb35bc034bc32012ee27", size = 96292, upload-time = "2025-10-06T14:12:15.398Z" }, - { url = "https://files.pythonhosted.org/packages/48/b7/503c98092fb3b344a179579f55814b613c1fbb1c23b3ec14a7b008a66a6e/yarl-1.22.0-cp314-cp314t-win_arm64.whl", hash = "sha256:9f6d73c1436b934e3f01df1e1b21ff765cd1d28c77dfb9ace207f746d4610ee1", size = 85171, upload-time = "2025-10-06T14:12:16.935Z" }, - { url = "https://files.pythonhosted.org/packages/73/ae/b48f95715333080afb75a4504487cbe142cae1268afc482d06692d605ae6/yarl-1.22.0-py3-none-any.whl", hash = "sha256:1380560bdba02b6b6c90de54133c81c9f2a453dee9912fe58c1dcced1edb7cff", size = 46814, upload-time = "2025-10-06T14:12:53.872Z" }, + { url = "https://files.pythonhosted.org/packages/9a/4b/a0a6e5d0ee8a2f3a373ddef8a4097d74ac901ac363eea1440464ccbe0898/yarl-1.23.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:16c6994ac35c3e74fb0ae93323bf8b9c2a9088d55946109489667c510a7d010e", size = 123796, upload-time = "2026-03-01T22:05:41.412Z" }, + { url = "https://files.pythonhosted.org/packages/67/b6/8925d68af039b835ae876db5838e82e76ec87b9782ecc97e192b809c4831/yarl-1.23.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4a42e651629dafb64fd5b0286a3580613702b5809ad3f24934ea87595804f2c5", size = 86547, upload-time = "2026-03-01T22:05:42.841Z" }, + { url = "https://files.pythonhosted.org/packages/ae/50/06d511cc4b8e0360d3c94af051a768e84b755c5eb031b12adaaab6dec6e5/yarl-1.23.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7c6b9461a2a8b47c65eef63bb1c76a4f1c119618ffa99ea79bc5bb1e46c5821b", size = 85854, upload-time = "2026-03-01T22:05:44.85Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f4/4e30b250927ffdab4db70da08b9b8d2194d7c7b400167b8fbeca1e4701ca/yarl-1.23.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2569b67d616eab450d262ca7cb9f9e19d2f718c70a8b88712859359d0ab17035", size = 98351, upload-time = "2026-03-01T22:05:46.836Z" }, + { url = "https://files.pythonhosted.org/packages/86/fc/4118c5671ea948208bdb1492d8b76bdf1453d3e73df051f939f563e7dcc5/yarl-1.23.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e9d9a4d06d3481eab79803beb4d9bd6f6a8e781ec078ac70d7ef2dcc29d1bea5", size = 92711, upload-time = "2026-03-01T22:05:48.316Z" }, + { url = "https://files.pythonhosted.org/packages/56/11/1ed91d42bd9e73c13dc9e7eb0dd92298d75e7ac4dd7f046ad0c472e231cd/yarl-1.23.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f514f6474e04179d3d33175ed3f3e31434d3130d42ec153540d5b157deefd735", size = 106014, upload-time = "2026-03-01T22:05:50.028Z" }, + { url = "https://files.pythonhosted.org/packages/ce/c9/74e44e056a23fbc33aca71779ef450ca648a5bc472bdad7a82339918f818/yarl-1.23.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fda207c815b253e34f7e1909840fd14299567b1c0eb4908f8c2ce01a41265401", size = 105557, upload-time = "2026-03-01T22:05:51.416Z" }, + { url = "https://files.pythonhosted.org/packages/66/fe/b1e10b08d287f518994f1e2ff9b6d26f0adeecd8dd7d533b01bab29a3eda/yarl-1.23.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34b6cf500e61c90f305094911f9acc9c86da1a05a7a3f5be9f68817043f486e4", size = 101559, upload-time = "2026-03-01T22:05:52.872Z" }, + { url = "https://files.pythonhosted.org/packages/72/59/c5b8d94b14e3d3c2a9c20cb100119fd534ab5a14b93673ab4cc4a4141ea5/yarl-1.23.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d7504f2b476d21653e4d143f44a175f7f751cd41233525312696c76aa3dbb23f", size = 100502, upload-time = "2026-03-01T22:05:54.954Z" }, + { url = "https://files.pythonhosted.org/packages/77/4f/96976cb54cbfc5c9fd73ed4c51804f92f209481d1fb190981c0f8a07a1d7/yarl-1.23.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:578110dd426f0d209d1509244e6d4a3f1a3e9077655d98c5f22583d63252a08a", size = 98027, upload-time = "2026-03-01T22:05:56.409Z" }, + { url = "https://files.pythonhosted.org/packages/63/6e/904c4f476471afdbad6b7e5b70362fb5810e35cd7466529a97322b6f5556/yarl-1.23.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:609d3614d78d74ebe35f54953c5bbd2ac647a7ddb9c30a5d877580f5e86b22f2", size = 95369, upload-time = "2026-03-01T22:05:58.141Z" }, + { url = "https://files.pythonhosted.org/packages/9d/40/acfcdb3b5f9d68ef499e39e04d25e141fe90661f9d54114556cf83be8353/yarl-1.23.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4966242ec68afc74c122f8459abd597afd7d8a60dc93d695c1334c5fd25f762f", size = 105565, upload-time = "2026-03-01T22:06:00.286Z" }, + { url = "https://files.pythonhosted.org/packages/5e/c6/31e28f3a6ba2869c43d124f37ea5260cac9c9281df803c354b31f4dd1f3c/yarl-1.23.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e0fd068364a6759bc794459f0a735ab151d11304346332489c7972bacbe9e72b", size = 99813, upload-time = "2026-03-01T22:06:01.712Z" }, + { url = "https://files.pythonhosted.org/packages/08/1f/6f65f59e72d54aa467119b63fc0b0b1762eff0232db1f4720cd89e2f4a17/yarl-1.23.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:39004f0ad156da43e86aa71f44e033de68a44e5a31fc53507b36dd253970054a", size = 105632, upload-time = "2026-03-01T22:06:03.188Z" }, + { url = "https://files.pythonhosted.org/packages/a3/c4/18b178a69935f9e7a338127d5b77d868fdc0f0e49becd286d51b3a18c61d/yarl-1.23.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e5723c01a56c5028c807c701aa66722916d2747ad737a046853f6c46f4875543", size = 101895, upload-time = "2026-03-01T22:06:04.651Z" }, + { url = "https://files.pythonhosted.org/packages/8f/54/f5b870b5505663911dba950a8e4776a0dbd51c9c54c0ae88e823e4b874a0/yarl-1.23.0-cp313-cp313-win32.whl", hash = "sha256:1b6b572edd95b4fa8df75de10b04bc81acc87c1c7d16bcdd2035b09d30acc957", size = 82356, upload-time = "2026-03-01T22:06:06.04Z" }, + { url = "https://files.pythonhosted.org/packages/7a/84/266e8da36879c6edcd37b02b547e2d9ecdfea776be49598e75696e3316e1/yarl-1.23.0-cp313-cp313-win_amd64.whl", hash = "sha256:baaf55442359053c7d62f6f8413a62adba3205119bcb6f49594894d8be47e5e3", size = 87515, upload-time = "2026-03-01T22:06:08.107Z" }, + { url = "https://files.pythonhosted.org/packages/00/fd/7e1c66efad35e1649114fa13f17485f62881ad58edeeb7f49f8c5e748bf9/yarl-1.23.0-cp313-cp313-win_arm64.whl", hash = "sha256:fb4948814a2a98e3912505f09c9e7493b1506226afb1f881825368d6fb776ee3", size = 81785, upload-time = "2026-03-01T22:06:10.181Z" }, + { url = "https://files.pythonhosted.org/packages/9c/fc/119dd07004f17ea43bb91e3ece6587759edd7519d6b086d16bfbd3319982/yarl-1.23.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:aecfed0b41aa72b7881712c65cf764e39ce2ec352324f5e0837c7048d9e6daaa", size = 130719, upload-time = "2026-03-01T22:06:11.708Z" }, + { url = "https://files.pythonhosted.org/packages/e6/0d/9f2348502fbb3af409e8f47730282cd6bc80dec6630c1e06374d882d6eb2/yarl-1.23.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a41bcf68efd19073376eb8cf948b8d9be0af26256403e512bb18f3966f1f9120", size = 89690, upload-time = "2026-03-01T22:06:13.429Z" }, + { url = "https://files.pythonhosted.org/packages/50/93/e88f3c80971b42cfc83f50a51b9d165a1dbf154b97005f2994a79f212a07/yarl-1.23.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cde9a2ecd91668bcb7f077c4966d8ceddb60af01b52e6e3e2680e4cf00ad1a59", size = 89851, upload-time = "2026-03-01T22:06:15.53Z" }, + { url = "https://files.pythonhosted.org/packages/1c/07/61c9dd8ba8f86473263b4036f70fb594c09e99c0d9737a799dfd8bc85651/yarl-1.23.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5023346c4ee7992febc0068e7593de5fa2bf611848c08404b35ebbb76b1b0512", size = 95874, upload-time = "2026-03-01T22:06:17.553Z" }, + { url = "https://files.pythonhosted.org/packages/9e/e9/f9ff8ceefba599eac6abddcfb0b3bee9b9e636e96dbf54342a8577252379/yarl-1.23.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d1009abedb49ae95b136a8904a3f71b342f849ffeced2d3747bf29caeda218c4", size = 88710, upload-time = "2026-03-01T22:06:19.004Z" }, + { url = "https://files.pythonhosted.org/packages/eb/78/0231bfcc5d4c8eec220bc2f9ef82cb4566192ea867a7c5b4148f44f6cbcd/yarl-1.23.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a8d00f29b42f534cc8aa3931cfe773b13b23e561e10d2b26f27a8d309b0e82a1", size = 101033, upload-time = "2026-03-01T22:06:21.203Z" }, + { url = "https://files.pythonhosted.org/packages/cd/9b/30ea5239a61786f18fd25797151a17fbb3be176977187a48d541b5447dd4/yarl-1.23.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:95451e6ce06c3e104556d73b559f5da6c34a069b6b62946d3ad66afcd51642ea", size = 100817, upload-time = "2026-03-01T22:06:22.738Z" }, + { url = "https://files.pythonhosted.org/packages/62/e2/a4980481071791bc83bce2b7a1a1f7adcabfa366007518b4b845e92eeee3/yarl-1.23.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:531ef597132086b6cf96faa7c6c1dcd0361dd5f1694e5cc30375907b9b7d3ea9", size = 97482, upload-time = "2026-03-01T22:06:24.21Z" }, + { url = "https://files.pythonhosted.org/packages/e5/1e/304a00cf5f6100414c4b5a01fc7ff9ee724b62158a08df2f8170dfc72a2d/yarl-1.23.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:88f9fb0116fbfcefcab70f85cf4b74a2b6ce5d199c41345296f49d974ddb4123", size = 95949, upload-time = "2026-03-01T22:06:25.697Z" }, + { url = "https://files.pythonhosted.org/packages/68/03/093f4055ed4cae649ac53bca3d180bd37102e9e11d048588e9ab0c0108d0/yarl-1.23.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e7b0460976dc75cb87ad9cc1f9899a4b97751e7d4e77ab840fc9b6d377b8fd24", size = 95839, upload-time = "2026-03-01T22:06:27.309Z" }, + { url = "https://files.pythonhosted.org/packages/b9/28/4c75ebb108f322aa8f917ae10a8ffa4f07cae10a8a627b64e578617df6a0/yarl-1.23.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:115136c4a426f9da976187d238e84139ff6b51a20839aa6e3720cd1026d768de", size = 90696, upload-time = "2026-03-01T22:06:29.048Z" }, + { url = "https://files.pythonhosted.org/packages/23/9c/42c2e2dd91c1a570402f51bdf066bfdb1241c2240ba001967bad778e77b7/yarl-1.23.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:ead11956716a940c1abc816b7df3fa2b84d06eaed8832ca32f5c5e058c65506b", size = 100865, upload-time = "2026-03-01T22:06:30.525Z" }, + { url = "https://files.pythonhosted.org/packages/74/05/1bcd60a8a0a914d462c305137246b6f9d167628d73568505fce3f1cb2e65/yarl-1.23.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:fe8f8f5e70e6dbdfca9882cd9deaac058729bcf323cf7a58660901e55c9c94f6", size = 96234, upload-time = "2026-03-01T22:06:32.692Z" }, + { url = "https://files.pythonhosted.org/packages/90/b2/f52381aac396d6778ce516b7bc149c79e65bfc068b5de2857ab69eeea3b7/yarl-1.23.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:a0e317df055958a0c1e79e5d2aa5a5eaa4a6d05a20d4b0c9c3f48918139c9fc6", size = 100295, upload-time = "2026-03-01T22:06:34.268Z" }, + { url = "https://files.pythonhosted.org/packages/e5/e8/638bae5bbf1113a659b2435d8895474598afe38b4a837103764f603aba56/yarl-1.23.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f0fd84de0c957b2d280143522c4f91a73aada1923caee763e24a2b3fda9f8a5", size = 97784, upload-time = "2026-03-01T22:06:35.864Z" }, + { url = "https://files.pythonhosted.org/packages/80/25/a3892b46182c586c202629fc2159aa13975d3741d52ebd7347fd501d48d5/yarl-1.23.0-cp313-cp313t-win32.whl", hash = "sha256:93a784271881035ab4406a172edb0faecb6e7d00f4b53dc2f55919d6c9688595", size = 88313, upload-time = "2026-03-01T22:06:37.39Z" }, + { url = "https://files.pythonhosted.org/packages/43/68/8c5b36aa5178900b37387937bc2c2fe0e9505537f713495472dcf6f6fccc/yarl-1.23.0-cp313-cp313t-win_amd64.whl", hash = "sha256:dd00607bffbf30250fe108065f07453ec124dbf223420f57f5e749b04295e090", size = 94932, upload-time = "2026-03-01T22:06:39.579Z" }, + { url = "https://files.pythonhosted.org/packages/c6/cc/d79ba8292f51f81f4dc533a8ccfb9fc6992cabf0998ed3245de7589dc07c/yarl-1.23.0-cp313-cp313t-win_arm64.whl", hash = "sha256:ac09d42f48f80c9ee1635b2fcaa819496a44502737660d3c0f2ade7526d29144", size = 84786, upload-time = "2026-03-01T22:06:41.988Z" }, + { url = "https://files.pythonhosted.org/packages/90/98/b85a038d65d1b92c3903ab89444f48d3cee490a883477b716d7a24b1a78c/yarl-1.23.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:21d1b7305a71a15b4794b5ff22e8eef96ff4a6d7f9657155e5aa419444b28912", size = 124455, upload-time = "2026-03-01T22:06:43.615Z" }, + { url = "https://files.pythonhosted.org/packages/39/54/bc2b45559f86543d163b6e294417a107bb87557609007c007ad889afec18/yarl-1.23.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:85610b4f27f69984932a7abbe52703688de3724d9f72bceb1cca667deff27474", size = 86752, upload-time = "2026-03-01T22:06:45.425Z" }, + { url = "https://files.pythonhosted.org/packages/24/f9/e8242b68362bffe6fb536c8db5076861466fc780f0f1b479fc4ffbebb128/yarl-1.23.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23f371bd662cf44a7630d4d113101eafc0cfa7518a2760d20760b26021454719", size = 86291, upload-time = "2026-03-01T22:06:46.974Z" }, + { url = "https://files.pythonhosted.org/packages/ea/d8/d1cb2378c81dd729e98c716582b1ccb08357e8488e4c24714658cc6630e8/yarl-1.23.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4a80f77dc1acaaa61f0934176fccca7096d9b1ff08c8ba9cddf5ae034a24319", size = 99026, upload-time = "2026-03-01T22:06:48.459Z" }, + { url = "https://files.pythonhosted.org/packages/0a/ff/7196790538f31debe3341283b5b0707e7feb947620fc5e8236ef28d44f72/yarl-1.23.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:bd654fad46d8d9e823afbb4f87c79160b5a374ed1ff5bde24e542e6ba8f41434", size = 92355, upload-time = "2026-03-01T22:06:50.306Z" }, + { url = "https://files.pythonhosted.org/packages/c1/56/25d58c3eddde825890a5fe6aa1866228377354a3c39262235234ab5f616b/yarl-1.23.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:682bae25f0a0dd23a056739f23a134db9f52a63e2afd6bfb37ddc76292bbd723", size = 106417, upload-time = "2026-03-01T22:06:52.1Z" }, + { url = "https://files.pythonhosted.org/packages/51/8a/882c0e7bc8277eb895b31bce0138f51a1ba551fc2e1ec6753ffc1e7c1377/yarl-1.23.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a82836cab5f197a0514235aaf7ffccdc886ccdaa2324bc0aafdd4ae898103039", size = 106422, upload-time = "2026-03-01T22:06:54.424Z" }, + { url = "https://files.pythonhosted.org/packages/42/2b/fef67d616931055bf3d6764885990a3ac647d68734a2d6a9e1d13de437a2/yarl-1.23.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c57676bdedc94cd3bc37724cf6f8cd2779f02f6aba48de45feca073e714fe52", size = 101915, upload-time = "2026-03-01T22:06:55.895Z" }, + { url = "https://files.pythonhosted.org/packages/18/6a/530e16aebce27c5937920f3431c628a29a4b6b430fab3fd1c117b26ff3f6/yarl-1.23.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c7f8dc16c498ff06497c015642333219871effba93e4a2e8604a06264aca5c5c", size = 100690, upload-time = "2026-03-01T22:06:58.21Z" }, + { url = "https://files.pythonhosted.org/packages/88/08/93749219179a45e27b036e03260fda05190b911de8e18225c294ac95bbc9/yarl-1.23.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:5ee586fb17ff8f90c91cf73c6108a434b02d69925f44f5f8e0d7f2f260607eae", size = 98750, upload-time = "2026-03-01T22:06:59.794Z" }, + { url = "https://files.pythonhosted.org/packages/d9/cf/ea424a004969f5d81a362110a6ac1496d79efdc6d50c2c4b2e3ea0fc2519/yarl-1.23.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:17235362f580149742739cc3828b80e24029d08cbb9c4bda0242c7b5bc610a8e", size = 94685, upload-time = "2026-03-01T22:07:01.375Z" }, + { url = "https://files.pythonhosted.org/packages/e2/b7/14341481fe568e2b0408bcf1484c652accafe06a0ade9387b5d3fd9df446/yarl-1.23.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:0793e2bd0cf14234983bbb371591e6bea9e876ddf6896cdcc93450996b0b5c85", size = 106009, upload-time = "2026-03-01T22:07:03.151Z" }, + { url = "https://files.pythonhosted.org/packages/0a/e6/5c744a9b54f4e8007ad35bce96fbc9218338e84812d36f3390cea616881a/yarl-1.23.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:3650dc2480f94f7116c364096bc84b1d602f44224ef7d5c7208425915c0475dd", size = 100033, upload-time = "2026-03-01T22:07:04.701Z" }, + { url = "https://files.pythonhosted.org/packages/0c/23/e3bfc188d0b400f025bc49d99793d02c9abe15752138dcc27e4eaf0c4a9e/yarl-1.23.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f40e782d49630ad384db66d4d8b73ff4f1b8955dc12e26b09a3e3af064b3b9d6", size = 106483, upload-time = "2026-03-01T22:07:06.231Z" }, + { url = "https://files.pythonhosted.org/packages/72/42/f0505f949a90b3f8b7a363d6cbdf398f6e6c58946d85c6d3a3bc70595b26/yarl-1.23.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:94f8575fbdf81749008d980c17796097e645574a3b8c28ee313931068dad14fe", size = 102175, upload-time = "2026-03-01T22:07:08.4Z" }, + { url = "https://files.pythonhosted.org/packages/aa/65/b39290f1d892a9dd671d1c722014ca062a9c35d60885d57e5375db0404b5/yarl-1.23.0-cp314-cp314-win32.whl", hash = "sha256:c8aa34a5c864db1087d911a0b902d60d203ea3607d91f615acd3f3108ac32169", size = 83871, upload-time = "2026-03-01T22:07:09.968Z" }, + { url = "https://files.pythonhosted.org/packages/a9/5b/9b92f54c784c26e2a422e55a8d2607ab15b7ea3349e28359282f84f01d43/yarl-1.23.0-cp314-cp314-win_amd64.whl", hash = "sha256:63e92247f383c85ab00dd0091e8c3fa331a96e865459f5ee80353c70a4a42d70", size = 89093, upload-time = "2026-03-01T22:07:11.501Z" }, + { url = "https://files.pythonhosted.org/packages/e0/7d/8a84dc9381fd4412d5e7ff04926f9865f6372b4c2fd91e10092e65d29eb8/yarl-1.23.0-cp314-cp314-win_arm64.whl", hash = "sha256:70efd20be968c76ece7baa8dafe04c5be06abc57f754d6f36f3741f7aa7a208e", size = 83384, upload-time = "2026-03-01T22:07:13.069Z" }, + { url = "https://files.pythonhosted.org/packages/dd/8d/d2fad34b1c08aa161b74394183daa7d800141aaaee207317e82c790b418d/yarl-1.23.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:9a18d6f9359e45722c064c97464ec883eb0e0366d33eda61cb19a244bf222679", size = 131019, upload-time = "2026-03-01T22:07:14.903Z" }, + { url = "https://files.pythonhosted.org/packages/19/ff/33009a39d3ccf4b94d7d7880dfe17fb5816c5a4fe0096d9b56abceea9ac7/yarl-1.23.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:2803ed8b21ca47a43da80a6fd1ed3019d30061f7061daa35ac54f63933409412", size = 89894, upload-time = "2026-03-01T22:07:17.372Z" }, + { url = "https://files.pythonhosted.org/packages/0c/f1/dab7ac5e7306fb79c0190766a3c00b4cb8d09a1f390ded68c85a5934faf5/yarl-1.23.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:394906945aa8b19fc14a61cf69743a868bb8c465efe85eee687109cc540b98f4", size = 89979, upload-time = "2026-03-01T22:07:19.361Z" }, + { url = "https://files.pythonhosted.org/packages/aa/b1/08e95f3caee1fad6e65017b9f26c1d79877b502622d60e517de01e72f95d/yarl-1.23.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:71d006bee8397a4a89f469b8deb22469fe7508132d3c17fa6ed871e79832691c", size = 95943, upload-time = "2026-03-01T22:07:21.266Z" }, + { url = "https://files.pythonhosted.org/packages/c0/cc/6409f9018864a6aa186c61175b977131f373f1988e198e031236916e87e4/yarl-1.23.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:62694e275c93d54f7ccedcfef57d42761b2aad5234b6be1f3e3026cae4001cd4", size = 88786, upload-time = "2026-03-01T22:07:23.129Z" }, + { url = "https://files.pythonhosted.org/packages/76/40/cc22d1d7714b717fde2006fad2ced5efe5580606cb059ae42117542122f3/yarl-1.23.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a31de1613658308efdb21ada98cbc86a97c181aa050ba22a808120bb5be3ab94", size = 101307, upload-time = "2026-03-01T22:07:24.689Z" }, + { url = "https://files.pythonhosted.org/packages/8f/0d/476c38e85ddb4c6ec6b20b815bdd779aa386a013f3d8b85516feee55c8dc/yarl-1.23.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fb1e8b8d66c278b21d13b0a7ca22c41dd757a7c209c6b12c313e445c31dd3b28", size = 100904, upload-time = "2026-03-01T22:07:26.287Z" }, + { url = "https://files.pythonhosted.org/packages/72/32/0abe4a76d59adf2081dcb0397168553ece4616ada1c54d1c49d8936c74f8/yarl-1.23.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50f9d8d531dfb767c565f348f33dd5139a6c43f5cbdf3f67da40d54241df93f6", size = 97728, upload-time = "2026-03-01T22:07:27.906Z" }, + { url = "https://files.pythonhosted.org/packages/b7/35/7b30f4810fba112f60f5a43237545867504e15b1c7647a785fbaf588fac2/yarl-1.23.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:575aa4405a656e61a540f4a80eaa5260f2a38fff7bfdc4b5f611840d76e9e277", size = 95964, upload-time = "2026-03-01T22:07:30.198Z" }, + { url = "https://files.pythonhosted.org/packages/2d/86/ed7a73ab85ef00e8bb70b0cb5421d8a2a625b81a333941a469a6f4022828/yarl-1.23.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:041b1a4cefacf65840b4e295c6985f334ba83c30607441ae3cf206a0eed1a2e4", size = 95882, upload-time = "2026-03-01T22:07:32.132Z" }, + { url = "https://files.pythonhosted.org/packages/19/90/d56967f61a29d8498efb7afb651e0b2b422a1e9b47b0ab5f4e40a19b699b/yarl-1.23.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:d38c1e8231722c4ce40d7593f28d92b5fc72f3e9774fe73d7e800ec32299f63a", size = 90797, upload-time = "2026-03-01T22:07:34.404Z" }, + { url = "https://files.pythonhosted.org/packages/72/00/8b8f76909259f56647adb1011d7ed8b321bcf97e464515c65016a47ecdf0/yarl-1.23.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:d53834e23c015ee83a99377db6e5e37d8484f333edb03bd15b4bc312cc7254fb", size = 101023, upload-time = "2026-03-01T22:07:35.953Z" }, + { url = "https://files.pythonhosted.org/packages/ac/e2/cab11b126fb7d440281b7df8e9ddbe4851e70a4dde47a202b6642586b8d9/yarl-1.23.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:2e27c8841126e017dd2a054a95771569e6070b9ee1b133366d8b31beb5018a41", size = 96227, upload-time = "2026-03-01T22:07:37.594Z" }, + { url = "https://files.pythonhosted.org/packages/c2/9b/2c893e16bfc50e6b2edf76c1a9eb6cb0c744346197e74c65e99ad8d634d0/yarl-1.23.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:76855800ac56f878847a09ce6dba727c93ca2d89c9e9d63002d26b916810b0a2", size = 100302, upload-time = "2026-03-01T22:07:39.334Z" }, + { url = "https://files.pythonhosted.org/packages/28/ec/5498c4e3a6d5f1003beb23405671c2eb9cdbf3067d1c80f15eeafe301010/yarl-1.23.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e09fd068c2e169a7070d83d3bde728a4d48de0549f975290be3c108c02e499b4", size = 98202, upload-time = "2026-03-01T22:07:41.717Z" }, + { url = "https://files.pythonhosted.org/packages/fe/c3/cd737e2d45e70717907f83e146f6949f20cc23cd4bf7b2688727763aa458/yarl-1.23.0-cp314-cp314t-win32.whl", hash = "sha256:73309162a6a571d4cbd3b6a1dcc703c7311843ae0d1578df6f09be4e98df38d4", size = 90558, upload-time = "2026-03-01T22:07:43.433Z" }, + { url = "https://files.pythonhosted.org/packages/e1/19/3774d162f6732d1cfb0b47b4140a942a35ca82bb19b6db1f80e9e7bdc8f8/yarl-1.23.0-cp314-cp314t-win_amd64.whl", hash = "sha256:4503053d296bc6e4cbd1fad61cf3b6e33b939886c4f249ba7c78b602214fabe2", size = 97610, upload-time = "2026-03-01T22:07:45.773Z" }, + { url = "https://files.pythonhosted.org/packages/51/47/3fa2286c3cb162c71cdb34c4224d5745a1ceceb391b2bd9b19b668a8d724/yarl-1.23.0-cp314-cp314t-win_arm64.whl", hash = "sha256:44bb7bef4ea409384e3f8bc36c063d77ea1b8d4a5b2706956c0d6695f07dcc25", size = 86041, upload-time = "2026-03-01T22:07:49.026Z" }, + { url = "https://files.pythonhosted.org/packages/69/68/c8739671f5699c7dc470580a4f821ef37c32c4cb0b047ce223a7f115757f/yarl-1.23.0-py3-none-any.whl", hash = "sha256:a2df6afe50dea8ae15fa34c9f824a3ee958d785fd5d089063d960bae1daa0a3f", size = 48288, upload-time = "2026-03-01T22:07:51.388Z" }, ]