- 替换 Field(max_length=X) 为 StrX/TextX 类型别名(21 个 sqlmodels 文件) - 替换 get + 404 检查为 get_exist_one()(17 个路由文件,约 50 处) - 替换 save + session.refresh 为 save(load=...) - 替换 session.add + commit 为 save()(dav/provider.py) - 更新所有依赖至最新版本 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -5,7 +5,8 @@
|
|||||||
"Bash(findstr:*)",
|
"Bash(findstr:*)",
|
||||||
"Bash(find:*)",
|
"Bash(find:*)",
|
||||||
"Bash(yarn tsc:*)",
|
"Bash(yarn tsc:*)",
|
||||||
"Bash(dir:*)"
|
"Bash(dir:*)",
|
||||||
|
"mcp__server-notify__notify"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
2
ee
2
ee
Submodule ee updated: 9dd69bd257...cc32d8db91
20
main.py
20
main.py
@@ -17,24 +17,26 @@ from utils.http.http_exceptions import raise_internal_error
|
|||||||
from utils.lifespan import lifespan
|
from utils.lifespan import lifespan
|
||||||
|
|
||||||
# 尝试加载企业版功能
|
# 尝试加载企业版功能
|
||||||
|
_has_ee: bool = False
|
||||||
try:
|
try:
|
||||||
from ee import init_ee
|
from ee import init_ee
|
||||||
from ee.license import LicenseError
|
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:
|
try:
|
||||||
await init_ee()
|
await init_ee()
|
||||||
except LicenseError as exc:
|
except LicenseError as exc:
|
||||||
l.critical(f"许可证验证失败: {exc}")
|
l.critical(f"许可证验证失败: {exc}")
|
||||||
raise SystemExit(1) from exc
|
raise SystemExit(1) from exc
|
||||||
|
|
||||||
from ee.routers import ee_router
|
lifespan.add_startup(_init_ee)
|
||||||
from routers.api.v1 import router as v1_router
|
except ImportError as exc:
|
||||||
v1_router.include_router(ee_router)
|
ee_router = None
|
||||||
|
l.info(f"以 Community 版本运行 (原因: {exc})")
|
||||||
lifespan.add_startup(_init_ee_and_routes)
|
|
||||||
except ImportError:
|
|
||||||
l.info("以 Community 版本运行")
|
|
||||||
|
|
||||||
STATICS_DIR: Path = (Path(__file__).parent / "statics").resolve()
|
STATICS_DIR: Path = (Path(__file__).parent / "statics").resolve()
|
||||||
"""前端静态文件目录(由 Docker 构建时复制)"""
|
"""前端静态文件目录(由 Docker 构建时复制)"""
|
||||||
@@ -95,6 +97,8 @@ async def handle_unexpected_exceptions(
|
|||||||
|
|
||||||
# 挂载路由
|
# 挂载路由
|
||||||
app.include_router(router)
|
app.include_router(router)
|
||||||
|
if _has_ee:
|
||||||
|
app.include_router(ee_router, prefix="/api/v1")
|
||||||
|
|
||||||
# 挂载 WebDAV 协议端点(优先于 SPA catch-all)
|
# 挂载 WebDAV 协议端点(优先于 SPA catch-all)
|
||||||
app.mount("/dav", dav_app)
|
app.mount("/dav", dav_app)
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ from utils.conf import appmeta
|
|||||||
from .admin import admin_router
|
from .admin import admin_router
|
||||||
|
|
||||||
from .callback import callback_router
|
from .callback import callback_router
|
||||||
|
from .category import category_router
|
||||||
from .directory import directory_router
|
from .directory import directory_router
|
||||||
from .download import download_router
|
from .download import download_router
|
||||||
from .file import router as file_router
|
from .file import router as file_router
|
||||||
@@ -14,7 +15,6 @@ from .trash import trash_router
|
|||||||
from .site import site_router
|
from .site import site_router
|
||||||
from .slave import slave_router
|
from .slave import slave_router
|
||||||
from .user import user_router
|
from .user import user_router
|
||||||
from .vas import vas_router
|
|
||||||
from .webdav import webdav_router
|
from .webdav import webdav_router
|
||||||
|
|
||||||
router = APIRouter(prefix="/v1")
|
router = APIRouter(prefix="/v1")
|
||||||
@@ -24,6 +24,7 @@ router = APIRouter(prefix="/v1")
|
|||||||
if appmeta.mode == "master":
|
if appmeta.mode == "master":
|
||||||
router.include_router(admin_router)
|
router.include_router(admin_router)
|
||||||
router.include_router(callback_router)
|
router.include_router(callback_router)
|
||||||
|
router.include_router(category_router)
|
||||||
router.include_router(directory_router)
|
router.include_router(directory_router)
|
||||||
router.include_router(download_router)
|
router.include_router(download_router)
|
||||||
router.include_router(file_router)
|
router.include_router(file_router)
|
||||||
@@ -32,7 +33,6 @@ if appmeta.mode == "master":
|
|||||||
router.include_router(site_router)
|
router.include_router(site_router)
|
||||||
router.include_router(trash_router)
|
router.include_router(trash_router)
|
||||||
router.include_router(user_router)
|
router.include_router(user_router)
|
||||||
router.include_router(vas_router)
|
|
||||||
router.include_router(webdav_router)
|
router.include_router(webdav_router)
|
||||||
elif appmeta.mode == "slave":
|
elif appmeta.mode == "slave":
|
||||||
router.include_router(slave_router)
|
router.include_router(slave_router)
|
||||||
|
|||||||
@@ -16,6 +16,12 @@ from sqlmodels.setting import (
|
|||||||
from sqlmodels.setting import SettingsType
|
from sqlmodels.setting import SettingsType
|
||||||
from utils import http_exceptions
|
from utils import http_exceptions
|
||||||
from utils.conf import appmeta
|
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 import admin_file_router
|
||||||
from .file_app import admin_file_app_router
|
from .file_app import admin_file_app_router
|
||||||
from .group import admin_group_router
|
from .group import admin_group_router
|
||||||
@@ -24,7 +30,6 @@ from .share import admin_share_router
|
|||||||
from .task import admin_task_router
|
from .task import admin_task_router
|
||||||
from .user import admin_user_router
|
from .user import admin_user_router
|
||||||
from .theme import admin_theme_router
|
from .theme import admin_theme_router
|
||||||
from .vas import admin_vas_router
|
|
||||||
|
|
||||||
|
|
||||||
class Aria2TestRequest(SQLModelBase):
|
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_share_router)
|
||||||
admin_router.include_router(admin_task_router)
|
admin_router.include_router(admin_task_router)
|
||||||
admin_router.include_router(admin_theme_router)
|
admin_router.include_router(admin_theme_router)
|
||||||
admin_router.include_router(admin_vas_router)
|
|
||||||
|
|
||||||
# 离线下载 /api/admin/aria2
|
# 离线下载 /api/admin/aria2
|
||||||
admin_aria2_router = APIRouter(
|
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:
|
if site_url_setting and site_url_setting.value:
|
||||||
site_urls.append(site_url_setting.value)
|
site_urls.append(site_url_setting.value)
|
||||||
|
|
||||||
# 许可证信息(从设置读取或使用默认值)
|
# 许可证信息(Pro 版本从缓存读取,CE 版本永不过期)
|
||||||
license_info = LicenseInfo(
|
if appmeta.IsPro and get_cached_license:
|
||||||
expired_at=now + timedelta(days=365),
|
payload = get_cached_license()
|
||||||
signed_at=now,
|
license_info = LicenseInfo(
|
||||||
root_domains=[],
|
expired_at=payload.expires_at,
|
||||||
domains=[],
|
signed_at=payload.issued_at,
|
||||||
vol_domains=[],
|
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(
|
version_info = VersionInfo(
|
||||||
@@ -225,11 +239,11 @@ async def router_admin_update_settings(
|
|||||||
|
|
||||||
if existing:
|
if existing:
|
||||||
existing.value = item.value
|
existing.value = item.value
|
||||||
await existing.save(session)
|
existing = await existing.save(session)
|
||||||
updated_count += 1
|
updated_count += 1
|
||||||
else:
|
else:
|
||||||
new_setting = Setting(type=item.type, name=item.name, value=item.value)
|
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
|
created_count += 1
|
||||||
|
|
||||||
l.info(f"管理员更新了 {updated_count} 个设置项,新建了 {created_count} 个设置项")
|
l.info(f"管理员更新了 {updated_count} 个设置项,新建了 {created_count} 个设置项")
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ async def _set_ban_recursive(
|
|||||||
obj.banned_by = None
|
obj.banned_by = None
|
||||||
obj.ban_reason = None
|
obj.ban_reason = None
|
||||||
|
|
||||||
await obj.save(session)
|
obj = await obj.save(session)
|
||||||
count += 1
|
count += 1
|
||||||
return count
|
return count
|
||||||
|
|
||||||
@@ -131,9 +131,7 @@ async def router_admin_preview_file(
|
|||||||
:param file_id: 文件UUID
|
:param file_id: 文件UUID
|
||||||
:return: 文件内容
|
:return: 文件内容
|
||||||
"""
|
"""
|
||||||
file_obj = await Object.get(session, Object.id == file_id)
|
file_obj = await Object.get_exist_one(session, file_id)
|
||||||
if not file_obj:
|
|
||||||
raise HTTPException(status_code=404, detail="文件不存在")
|
|
||||||
|
|
||||||
if not file_obj.is_file:
|
if not file_obj.is_file:
|
||||||
raise HTTPException(status_code=400, detail="对象不是文件")
|
raise HTTPException(status_code=400, detail="对象不是文件")
|
||||||
@@ -182,9 +180,7 @@ async def router_admin_ban_file(
|
|||||||
:param claims: 当前管理员 JWT claims
|
:param claims: 当前管理员 JWT claims
|
||||||
:return: 封禁结果
|
:return: 封禁结果
|
||||||
"""
|
"""
|
||||||
file_obj = await Object.get(session, Object.id == file_id)
|
file_obj = await Object.get_exist_one(session, file_id)
|
||||||
if not file_obj:
|
|
||||||
raise HTTPException(status_code=404, detail="文件不存在")
|
|
||||||
|
|
||||||
count = await _set_ban_recursive(session, file_obj, request.ban, claims.sub, request.reason)
|
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: 是否同时删除物理文件
|
:param delete_physical: 是否同时删除物理文件
|
||||||
:return: 删除结果
|
:return: 删除结果
|
||||||
"""
|
"""
|
||||||
file_obj = await Object.get(session, Object.id == file_id)
|
file_obj = await Object.get_exist_one(session, file_id)
|
||||||
if not file_obj:
|
|
||||||
raise HTTPException(status_code=404, detail="文件不存在")
|
|
||||||
|
|
||||||
if not file_obj.is_file:
|
if not file_obj.is_file:
|
||||||
raise HTTPException(status_code=400, detail="对象不是文件")
|
raise HTTPException(status_code=400, detail="对象不是文件")
|
||||||
|
|||||||
@@ -151,9 +151,7 @@ async def get_file_app(
|
|||||||
错误处理:
|
错误处理:
|
||||||
- 404: 应用不存在
|
- 404: 应用不存在
|
||||||
"""
|
"""
|
||||||
app: FileApp | None = await FileApp.get(session, FileApp.id == app_id)
|
app = await FileApp.get_exist_one(session, app_id)
|
||||||
if not app:
|
|
||||||
http_exceptions.raise_not_found("应用不存在")
|
|
||||||
|
|
||||||
extensions = await FileAppExtension.get(
|
extensions = await FileAppExtension.get(
|
||||||
session,
|
session,
|
||||||
@@ -186,9 +184,7 @@ async def update_file_app(
|
|||||||
- 404: 应用不存在
|
- 404: 应用不存在
|
||||||
- 409: 新 app_key 已被其他应用使用
|
- 409: 新 app_key 已被其他应用使用
|
||||||
"""
|
"""
|
||||||
app: FileApp | None = await FileApp.get(session, FileApp.id == app_id)
|
app = await FileApp.get_exist_one(session, app_id)
|
||||||
if not app:
|
|
||||||
http_exceptions.raise_not_found("应用不存在")
|
|
||||||
|
|
||||||
# 检查 app_key 唯一性
|
# 检查 app_key 唯一性
|
||||||
if request.app_key is not None and request.app_key != app.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: 应用不存在
|
- 404: 应用不存在
|
||||||
"""
|
"""
|
||||||
app: FileApp | None = await FileApp.get(session, FileApp.id == app_id)
|
app = await FileApp.get_exist_one(session, app_id)
|
||||||
if not app:
|
|
||||||
http_exceptions.raise_not_found("应用不存在")
|
|
||||||
|
|
||||||
app_name = app.app_key
|
app_name = app.app_key
|
||||||
await FileApp.delete(session, app)
|
await FileApp.delete(session, app)
|
||||||
@@ -263,9 +257,7 @@ async def update_extensions(
|
|||||||
错误处理:
|
错误处理:
|
||||||
- 404: 应用不存在
|
- 404: 应用不存在
|
||||||
"""
|
"""
|
||||||
app: FileApp | None = await FileApp.get(session, FileApp.id == app_id)
|
app = await FileApp.get_exist_one(session, app_id)
|
||||||
if not app:
|
|
||||||
http_exceptions.raise_not_found("应用不存在")
|
|
||||||
|
|
||||||
# 保留旧扩展名的 wopi_action_url(Discovery 填充的值)
|
# 保留旧扩展名的 wopi_action_url(Discovery 填充的值)
|
||||||
old_extensions: list[FileAppExtension] = await FileAppExtension.get(
|
old_extensions: list[FileAppExtension] = await FileAppExtension.get(
|
||||||
@@ -330,9 +322,7 @@ async def update_group_access(
|
|||||||
错误处理:
|
错误处理:
|
||||||
- 404: 应用不存在
|
- 404: 应用不存在
|
||||||
"""
|
"""
|
||||||
app: FileApp | None = await FileApp.get(session, FileApp.id == app_id)
|
await FileApp.get_exist_one(session, app_id)
|
||||||
if not app:
|
|
||||||
http_exceptions.raise_not_found("应用不存在")
|
|
||||||
|
|
||||||
# 删除旧的用户组关联
|
# 删除旧的用户组关联
|
||||||
old_links_result = await session.exec(
|
old_links_result = await session.exec(
|
||||||
@@ -387,9 +377,7 @@ async def discover_wopi(
|
|||||||
- 400: 非 WOPI 类型 / discovery URL 未配置 / XML 解析失败
|
- 400: 非 WOPI 类型 / discovery URL 未配置 / XML 解析失败
|
||||||
- 502: WOPI 服务端不可达或返回无效响应
|
- 502: WOPI 服务端不可达或返回无效响应
|
||||||
"""
|
"""
|
||||||
app: FileApp | None = await FileApp.get(session, FileApp.id == app_id)
|
app = await FileApp.get_exist_one(session, app_id)
|
||||||
if not app:
|
|
||||||
http_exceptions.raise_not_found("应用不存在")
|
|
||||||
|
|
||||||
if app.type != FileAppType.WOPI:
|
if app.type != FileAppType.WOPI:
|
||||||
http_exceptions.raise_bad_request("仅 WOPI 类型应用支持自动发现")
|
http_exceptions.raise_bad_request("仅 WOPI 类型应用支持自动发现")
|
||||||
|
|||||||
@@ -63,10 +63,7 @@ async def router_admin_get_group(
|
|||||||
:param group_id: 用户组UUID
|
:param group_id: 用户组UUID
|
||||||
:return: 用户组详情
|
:return: 用户组详情
|
||||||
"""
|
"""
|
||||||
group = await Group.get(session, Group.id == group_id, load=[Group.options, Group.policies])
|
group = await Group.get_exist_one(session, group_id, load=[Group.options, Group.policies])
|
||||||
|
|
||||||
if not group:
|
|
||||||
raise HTTPException(status_code=404, detail="用户组不存在")
|
|
||||||
|
|
||||||
# 直接访问已加载的关系,无需额外查询
|
# 直接访问已加载的关系,无需额外查询
|
||||||
policies = group.policies
|
policies = group.policies
|
||||||
@@ -94,9 +91,7 @@ async def router_admin_get_group_members(
|
|||||||
:return: 分页成员列表
|
:return: 分页成员列表
|
||||||
"""
|
"""
|
||||||
# 验证组存在
|
# 验证组存在
|
||||||
group = await Group.get(session, Group.id == group_id)
|
await Group.get_exist_one(session, group_id)
|
||||||
if not group:
|
|
||||||
raise HTTPException(status_code=404, detail="用户组不存在")
|
|
||||||
|
|
||||||
result = await User.get_with_count(session, User.group_id == group_id, table_view=table_view)
|
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,
|
speed_limit=request.speed_limit,
|
||||||
)
|
)
|
||||||
group = await group.save(session)
|
group = await group.save(session)
|
||||||
|
group_id_val: UUID = group.id
|
||||||
|
|
||||||
# 创建选项
|
# 创建选项
|
||||||
options = GroupOptions(
|
options = GroupOptions(
|
||||||
group_id=group.id,
|
group_id=group_id_val,
|
||||||
share_download=request.share_download,
|
share_download=request.share_download,
|
||||||
share_free=request.share_free,
|
share_free=request.share_free,
|
||||||
relocate=request.relocate,
|
relocate=request.relocate,
|
||||||
@@ -154,11 +150,11 @@ async def router_admin_create_group(
|
|||||||
aria2=request.aria2,
|
aria2=request.aria2,
|
||||||
redirected_source=request.redirected_source,
|
redirected_source=request.redirected_source,
|
||||||
)
|
)
|
||||||
await options.save(session)
|
options = await options.save(session)
|
||||||
|
|
||||||
# 关联存储策略
|
# 关联存储策略
|
||||||
for policy_id in request.policy_ids:
|
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)
|
session.add(link)
|
||||||
await session.commit()
|
await session.commit()
|
||||||
|
|
||||||
@@ -185,9 +181,7 @@ async def router_admin_update_group(
|
|||||||
:param request: 更新请求
|
:param request: 更新请求
|
||||||
:return: 更新结果
|
:return: 更新结果
|
||||||
"""
|
"""
|
||||||
group = await Group.get(session, Group.id == group_id, load=Group.options)
|
group = await Group.get_exist_one(session, group_id, load=Group.options)
|
||||||
if not group:
|
|
||||||
raise HTTPException(status_code=404, detail="用户组不存在")
|
|
||||||
|
|
||||||
# 检查名称唯一性(如果要更新名称)
|
# 检查名称唯一性(如果要更新名称)
|
||||||
if request.name and request.name != group.name:
|
if request.name and request.name != group.name:
|
||||||
@@ -217,7 +211,7 @@ async def router_admin_update_group(
|
|||||||
if options_data:
|
if options_data:
|
||||||
for key, value in options_data.items():
|
for key, value in options_data.items():
|
||||||
setattr(group.options, key, value)
|
setattr(group.options, key, value)
|
||||||
await group.options.save(session)
|
group.options = await group.options.save(session)
|
||||||
|
|
||||||
# 更新策略关联
|
# 更新策略关联
|
||||||
if request.policy_ids is not None:
|
if request.policy_ids is not None:
|
||||||
@@ -255,9 +249,7 @@ async def router_admin_delete_group(
|
|||||||
:param group_id: 用户组UUID
|
:param group_id: 用户组UUID
|
||||||
:return: 删除结果
|
:return: 删除结果
|
||||||
"""
|
"""
|
||||||
group = await Group.get(session, Group.id == group_id)
|
group = await Group.get_exist_one(session, group_id)
|
||||||
if not group:
|
|
||||||
raise HTTPException(status_code=404, detail="用户组不存在")
|
|
||||||
|
|
||||||
# 检查是否有用户属于该组
|
# 检查是否有用户属于该组
|
||||||
user_count = await User.count(session, User.group_id == group_id)
|
user_count = await User.count(session, User.group_id == group_id)
|
||||||
|
|||||||
@@ -332,7 +332,7 @@ async def router_policy_add_policy(
|
|||||||
s3_path_style=request.s3_path_style,
|
s3_path_style=request.s3_path_style,
|
||||||
s3_region=request.s3_region,
|
s3_region=request.s3_region,
|
||||||
)
|
)
|
||||||
await options.save(session)
|
options = await options.save(session)
|
||||||
|
|
||||||
@admin_policy_router.post(
|
@admin_policy_router.post(
|
||||||
path='/cors',
|
path='/cors',
|
||||||
@@ -383,9 +383,7 @@ async def router_policy_onddrive_oauth(
|
|||||||
:param policy_id: 存储策略UUID
|
:param policy_id: 存储策略UUID
|
||||||
:return: OAuth URL
|
:return: OAuth URL
|
||||||
"""
|
"""
|
||||||
policy = await Policy.get(session, Policy.id == policy_id)
|
policy = await Policy.get_exist_one(session, policy_id)
|
||||||
if not policy:
|
|
||||||
raise HTTPException(status_code=404, detail="存储策略不存在")
|
|
||||||
|
|
||||||
# TODO: 实现OneDrive OAuth
|
# TODO: 实现OneDrive OAuth
|
||||||
raise HTTPException(status_code=501, detail="OneDrive OAuth暂未实现")
|
raise HTTPException(status_code=501, detail="OneDrive OAuth暂未实现")
|
||||||
@@ -408,9 +406,7 @@ async def router_policy_get_policy(
|
|||||||
:param policy_id: 存储策略UUID
|
:param policy_id: 存储策略UUID
|
||||||
:return: 策略详情
|
:return: 策略详情
|
||||||
"""
|
"""
|
||||||
policy = await Policy.get(session, Policy.id == policy_id, load=Policy.options)
|
policy = await Policy.get_exist_one(session, policy_id, load=Policy.options)
|
||||||
if not policy:
|
|
||||||
raise HTTPException(status_code=404, detail="存储策略不存在")
|
|
||||||
|
|
||||||
# 获取使用此策略的用户组
|
# 获取使用此策略的用户组
|
||||||
groups = await policy.awaitable_attrs.groups
|
groups = await policy.awaitable_attrs.groups
|
||||||
@@ -459,9 +455,7 @@ async def router_policy_delete_policy(
|
|||||||
:param policy_id: 存储策略UUID
|
:param policy_id: 存储策略UUID
|
||||||
:return: 删除结果
|
:return: 删除结果
|
||||||
"""
|
"""
|
||||||
policy = await Policy.get(session, Policy.id == policy_id)
|
policy = await Policy.get_exist_one(session, policy_id)
|
||||||
if not policy:
|
|
||||||
raise HTTPException(status_code=404, detail="存储策略不存在")
|
|
||||||
|
|
||||||
# 检查是否有文件使用此策略
|
# 检查是否有文件使用此策略
|
||||||
file_count = await Object.count(session, Object.policy_id == 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 policy_id: 存储策略UUID
|
||||||
:param request: 更新请求
|
:param request: 更新请求
|
||||||
"""
|
"""
|
||||||
policy = await Policy.get(session, Policy.id == policy_id, load=Policy.options)
|
policy = await Policy.get_exist_one(session, policy_id, load=Policy.options)
|
||||||
if not policy:
|
|
||||||
raise HTTPException(status_code=404, detail="存储策略不存在")
|
|
||||||
|
|
||||||
# 检查名称唯一性(如果要更新名称)
|
# 检查名称唯一性(如果要更新名称)
|
||||||
if request.name and request.name != policy.name:
|
if request.name and request.name != policy.name:
|
||||||
@@ -529,10 +521,10 @@ async def router_policy_update_policy(
|
|||||||
if policy.options:
|
if policy.options:
|
||||||
for key, value in options_data.items():
|
for key, value in options_data.items():
|
||||||
setattr(policy.options, key, value)
|
setattr(policy.options, key, value)
|
||||||
await policy.options.save(session)
|
policy.options = await policy.options.save(session)
|
||||||
else:
|
else:
|
||||||
options = PolicyOptions(policy_id=policy.id, **options_data)
|
options = PolicyOptions(policy_id=policy.id, **options_data)
|
||||||
await options.save(session)
|
options = await options.save(session)
|
||||||
|
|
||||||
l.info(f"管理员更新了存储策略: {policy_id}")
|
l.info(f"管理员更新了存储策略: {policy_id}")
|
||||||
|
|
||||||
|
|||||||
@@ -155,9 +155,7 @@ async def router_admin_delete_share(
|
|||||||
:param share_id: 分享ID
|
:param share_id: 分享ID
|
||||||
:return: 删除结果
|
:return: 删除结果
|
||||||
"""
|
"""
|
||||||
share = await Share.get(session, Share.id == share_id)
|
share = await Share.get_exist_one(session, share_id)
|
||||||
if not share:
|
|
||||||
raise HTTPException(status_code=404, detail="分享不存在")
|
|
||||||
|
|
||||||
await Share.delete(session, share)
|
await Share.delete(session, share)
|
||||||
|
|
||||||
|
|||||||
@@ -150,9 +150,7 @@ async def router_admin_delete_task(
|
|||||||
:param task_id: 任务ID
|
:param task_id: 任务ID
|
||||||
:return: 删除结果
|
:return: 删除结果
|
||||||
"""
|
"""
|
||||||
task = await Task.get(session, Task.id == task_id)
|
task = await Task.get_exist_one(session, task_id)
|
||||||
if not task:
|
|
||||||
raise HTTPException(status_code=404, detail="任务不存在")
|
|
||||||
|
|
||||||
await Task.delete(session, task)
|
await Task.delete(session, task)
|
||||||
|
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ async def router_admin_theme_create(
|
|||||||
name=request.name,
|
name=request.name,
|
||||||
**request.colors.model_dump(),
|
**request.colors.model_dump(),
|
||||||
)
|
)
|
||||||
await preset.save(session)
|
preset = await preset.save(session)
|
||||||
l.info(f"管理员创建了主题预设: {request.name}")
|
l.info(f"管理员创建了主题预设: {request.name}")
|
||||||
|
|
||||||
|
|
||||||
@@ -101,11 +101,7 @@ async def router_admin_theme_update(
|
|||||||
- 404: 预设不存在
|
- 404: 预设不存在
|
||||||
- 409: 名称已被其他预设使用
|
- 409: 名称已被其他预设使用
|
||||||
"""
|
"""
|
||||||
preset: ThemePreset | None = await ThemePreset.get(
|
preset = await ThemePreset.get_exist_one(session, preset_id)
|
||||||
session, ThemePreset.id == preset_id
|
|
||||||
)
|
|
||||||
if not preset:
|
|
||||||
http_exceptions.raise_not_found("主题预设不存在")
|
|
||||||
|
|
||||||
# 检查名称唯一性(排除自身)
|
# 检查名称唯一性(排除自身)
|
||||||
if request.name is not None and request.name != preset.name:
|
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():
|
for key, value in color_data.items():
|
||||||
setattr(preset, key, value)
|
setattr(preset, key, value)
|
||||||
|
|
||||||
await preset.save(session)
|
preset = await preset.save(session)
|
||||||
l.info(f"管理员更新了主题预设: {preset.name}")
|
l.info(f"管理员更新了主题预设: {preset.name}")
|
||||||
|
|
||||||
|
|
||||||
@@ -147,11 +143,7 @@ async def router_admin_theme_delete(
|
|||||||
副作用:
|
副作用:
|
||||||
- 关联用户的 theme_preset_id 会被数据库 SET NULL
|
- 关联用户的 theme_preset_id 会被数据库 SET NULL
|
||||||
"""
|
"""
|
||||||
preset: ThemePreset | None = await ThemePreset.get(
|
preset = await ThemePreset.get_exist_one(session, preset_id)
|
||||||
session, ThemePreset.id == preset_id
|
|
||||||
)
|
|
||||||
if not preset:
|
|
||||||
http_exceptions.raise_not_found("主题预设不存在")
|
|
||||||
|
|
||||||
await preset.delete(session)
|
await preset.delete(session)
|
||||||
l.info(f"管理员删除了主题预设: {preset.name}")
|
l.info(f"管理员删除了主题预设: {preset.name}")
|
||||||
@@ -180,11 +172,7 @@ async def router_admin_theme_set_default(
|
|||||||
逻辑:
|
逻辑:
|
||||||
- 事务中先清除所有旧默认,再设新默认
|
- 事务中先清除所有旧默认,再设新默认
|
||||||
"""
|
"""
|
||||||
preset: ThemePreset | None = await ThemePreset.get(
|
preset = await ThemePreset.get_exist_one(session, preset_id)
|
||||||
session, ThemePreset.id == preset_id
|
|
||||||
)
|
|
||||||
if not preset:
|
|
||||||
http_exceptions.raise_not_found("主题预设不存在")
|
|
||||||
|
|
||||||
# 清除所有旧默认
|
# 清除所有旧默认
|
||||||
await session.execute(
|
await session.execute(
|
||||||
@@ -195,5 +183,5 @@ async def router_admin_theme_set_default(
|
|||||||
|
|
||||||
# 设新默认
|
# 设新默认
|
||||||
preset.is_default = True
|
preset.is_default = True
|
||||||
await preset.save(session)
|
preset = await preset.save(session)
|
||||||
l.info(f"管理员将主题预设 '{preset.name}' 设为默认")
|
l.info(f"管理员将主题预设 '{preset.name}' 设为默认")
|
||||||
|
|||||||
@@ -128,8 +128,9 @@ async def router_admin_create_user(
|
|||||||
is_verified=True,
|
is_verified=True,
|
||||||
user_id=user.id,
|
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()
|
return user.to_public()
|
||||||
|
|
||||||
|
|
||||||
@@ -153,9 +154,7 @@ async def router_admin_update_user(
|
|||||||
:param request: 更新请求
|
:param request: 更新请求
|
||||||
:return: 更新结果
|
:return: 更新结果
|
||||||
"""
|
"""
|
||||||
user = await User.get(session, User.id == user_id)
|
user = await User.get_exist_one(session, user_id)
|
||||||
if not user:
|
|
||||||
raise HTTPException(status_code=404, detail="用户不存在")
|
|
||||||
|
|
||||||
# 默认管理员不允许更改用户组(通过 Setting 中的 default_admin_id 识别)
|
# 默认管理员不允许更改用户组(通过 Setting 中的 default_admin_id 识别)
|
||||||
default_admin_setting = await Setting.get(
|
default_admin_setting = await Setting.get(
|
||||||
@@ -252,9 +251,7 @@ async def router_admin_calibrate_storage(
|
|||||||
:param user_id: 用户UUID
|
:param user_id: 用户UUID
|
||||||
:return: 校准结果
|
:return: 校准结果
|
||||||
"""
|
"""
|
||||||
user = await User.get(session, User.id == user_id)
|
user = await User.get_exist_one(session, user_id)
|
||||||
if not user:
|
|
||||||
raise HTTPException(status_code=404, detail="用户不存在")
|
|
||||||
|
|
||||||
previous_storage = user.storage
|
previous_storage = user.storage
|
||||||
|
|
||||||
|
|||||||
@@ -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="增值服务管理暂未实现")
|
|
||||||
@@ -16,18 +16,12 @@ oauth_router = APIRouter(
|
|||||||
tags=["callback", "oauth"],
|
tags=["callback", "oauth"],
|
||||||
)
|
)
|
||||||
|
|
||||||
pay_router = APIRouter(
|
|
||||||
prefix='/callback/pay',
|
|
||||||
tags=["callback", "pay"],
|
|
||||||
)
|
|
||||||
|
|
||||||
upload_router = APIRouter(
|
upload_router = APIRouter(
|
||||||
prefix='/callback/upload',
|
prefix='/callback/upload',
|
||||||
tags=["callback", "upload"],
|
tags=["callback", "upload"],
|
||||||
)
|
)
|
||||||
|
|
||||||
callback_router.include_router(oauth_router)
|
callback_router.include_router(oauth_router)
|
||||||
callback_router.include_router(pay_router)
|
|
||||||
callback_router.include_router(upload_router)
|
callback_router.include_router(upload_router)
|
||||||
|
|
||||||
@oauth_router.post(
|
@oauth_router.post(
|
||||||
@@ -38,7 +32,7 @@ callback_router.include_router(upload_router)
|
|||||||
def router_callback_qq() -> ResponseBase:
|
def router_callback_qq() -> ResponseBase:
|
||||||
"""
|
"""
|
||||||
Handle QQ OAuth callback and return user information.
|
Handle QQ OAuth callback and return user information.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
ResponseBase: A model containing the response data for the QQ OAuth callback.
|
ResponseBase: A model containing the response data for the QQ OAuth callback.
|
||||||
"""
|
"""
|
||||||
@@ -55,11 +49,11 @@ async def router_callback_github(
|
|||||||
GitHub OAuth 回调处理
|
GitHub OAuth 回调处理
|
||||||
- 错误响应示例:
|
- 错误响应示例:
|
||||||
- {
|
- {
|
||||||
'error': 'bad_verification_code',
|
'error': 'bad_verification_code',
|
||||||
'error_description': 'The code passed is incorrect or expired.',
|
'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'
|
'error_uri': 'https://docs.github.com/apps/managing-oauth-apps/troubleshooting-oauth-app-access-token-request-errors/#bad-verification-code'
|
||||||
}
|
}
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
PlainTextResponse: A response containing the user information from GitHub.
|
PlainTextResponse: A response containing the user information from GitHub.
|
||||||
"""
|
"""
|
||||||
@@ -77,81 +71,6 @@ async def router_callback_github(
|
|||||||
l.error(f"GitHub OAuth 回调异常: {e}")
|
l.error(f"GitHub OAuth 回调异常: {e}")
|
||||||
return PlainTextResponse("认证过程中发生错误,请重试", status_code=500)
|
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(
|
@upload_router.post(
|
||||||
path='/remote/{session_id}/{key}',
|
path='/remote/{session_id}/{key}',
|
||||||
summary='远程上传回调',
|
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:
|
def router_callback_remote(session_id: str, key: str) -> ResponseBase:
|
||||||
"""
|
"""
|
||||||
Handle remote upload callback and return upload status.
|
Handle remote upload callback and return upload status.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
session_id (str): The session ID for the upload.
|
session_id (str): The session ID for the upload.
|
||||||
key (str): The key for the uploaded file.
|
key (str): The key for the uploaded file.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
ResponseBase: A model containing the response data for the remote upload callback.
|
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:
|
def router_callback_qiniu(session_id: str) -> ResponseBase:
|
||||||
"""
|
"""
|
||||||
Handle Qiniu Cloud upload callback and return upload status.
|
Handle Qiniu Cloud upload callback and return upload status.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
session_id (str): The session ID for the upload.
|
session_id (str): The session ID for the upload.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
ResponseBase: A model containing the response data for the Qiniu Cloud upload callback.
|
ResponseBase: A model containing the response data for the Qiniu Cloud upload callback.
|
||||||
"""
|
"""
|
||||||
http_exceptions.raise_not_implemented()
|
http_exceptions.raise_not_implemented()
|
||||||
|
|
||||||
@upload_router.post(
|
@upload_router.post(
|
||||||
path='/tencent/{session_id}',
|
path='/tencent/{session_id}',
|
||||||
summary='腾讯云上传回调',
|
summary='腾讯云上传回调',
|
||||||
@@ -195,16 +114,16 @@ def router_callback_qiniu(session_id: str) -> ResponseBase:
|
|||||||
def router_callback_tencent(session_id: str) -> ResponseBase:
|
def router_callback_tencent(session_id: str) -> ResponseBase:
|
||||||
"""
|
"""
|
||||||
Handle Tencent Cloud upload callback and return upload status.
|
Handle Tencent Cloud upload callback and return upload status.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
session_id (str): The session ID for the upload.
|
session_id (str): The session ID for the upload.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
ResponseBase: A model containing the response data for the Tencent Cloud upload callback.
|
ResponseBase: A model containing the response data for the Tencent Cloud upload callback.
|
||||||
"""
|
"""
|
||||||
http_exceptions.raise_not_implemented()
|
http_exceptions.raise_not_implemented()
|
||||||
|
|
||||||
@upload_router.post(
|
@upload_router.post(
|
||||||
path='/aliyun/{session_id}',
|
path='/aliyun/{session_id}',
|
||||||
summary='阿里云上传回调',
|
summary='阿里云上传回调',
|
||||||
description='Handle Aliyun upload callback and return upload status.',
|
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:
|
def router_callback_aliyun(session_id: str) -> ResponseBase:
|
||||||
"""
|
"""
|
||||||
Handle Aliyun upload callback and return upload status.
|
Handle Aliyun upload callback and return upload status.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
session_id (str): The session ID for the upload.
|
session_id (str): The session ID for the upload.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
ResponseBase: A model containing the response data for the Aliyun upload callback.
|
ResponseBase: A model containing the response data for the Aliyun upload callback.
|
||||||
"""
|
"""
|
||||||
http_exceptions.raise_not_implemented()
|
http_exceptions.raise_not_implemented()
|
||||||
|
|
||||||
@upload_router.post(
|
@upload_router.post(
|
||||||
path='/upyun/{session_id}',
|
path='/upyun/{session_id}',
|
||||||
summary='又拍云上传回调',
|
summary='又拍云上传回调',
|
||||||
description='Handle Upyun upload callback and return upload status.',
|
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:
|
def router_callback_upyun(session_id: str) -> ResponseBase:
|
||||||
"""
|
"""
|
||||||
Handle Upyun upload callback and return upload status.
|
Handle Upyun upload callback and return upload status.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
session_id (str): The session ID for the upload.
|
session_id (str): The session ID for the upload.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
ResponseBase: A model containing the response data for the Upyun upload callback.
|
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:
|
def router_callback_aws(session_id: str) -> ResponseBase:
|
||||||
"""
|
"""
|
||||||
Handle AWS S3 upload callback and return upload status.
|
Handle AWS S3 upload callback and return upload status.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
session_id (str): The session ID for the upload.
|
session_id (str): The session ID for the upload.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
ResponseBase: A model containing the response data for the AWS S3 upload callback.
|
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:
|
def router_callback_onedrive_finish(session_id: str) -> ResponseBase:
|
||||||
"""
|
"""
|
||||||
Handle OneDrive upload completion callback and return upload status.
|
Handle OneDrive upload completion callback and return upload status.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
session_id (str): The session ID for the upload.
|
session_id (str): The session ID for the upload.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
ResponseBase: A model containing the response data for the OneDrive upload completion callback.
|
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:
|
def router_callback_onedrive_auth() -> ResponseBase:
|
||||||
"""
|
"""
|
||||||
Handle OneDrive authorization callback and return authorization status.
|
Handle OneDrive authorization callback and return authorization status.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
ResponseBase: A model containing the response data for the OneDrive authorization callback.
|
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:
|
def router_callback_google_auth() -> ResponseBase:
|
||||||
"""
|
"""
|
||||||
Handle Google OAuth completion callback and return authorization status.
|
Handle Google OAuth completion callback and return authorization status.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
ResponseBase: A model containing the response data for the Google OAuth completion callback.
|
ResponseBase: A model containing the response data for the Google OAuth completion callback.
|
||||||
"""
|
"""
|
||||||
http_exceptions.raise_not_implemented()
|
http_exceptions.raise_not_implemented()
|
||||||
|
|||||||
100
routers/api/v1/category/__init__.py
Normal file
100
routers/api/v1/category/__init__.py
Normal file
@@ -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)
|
||||||
@@ -206,4 +206,4 @@ async def router_directory_create(
|
|||||||
parent_id=parent_id,
|
parent_id=parent_id,
|
||||||
policy_id=policy_id,
|
policy_id=policy_id,
|
||||||
)
|
)
|
||||||
await new_folder.save(session)
|
new_folder = await new_folder.save(session)
|
||||||
|
|||||||
@@ -184,9 +184,7 @@ async def create_upload_session(
|
|||||||
|
|
||||||
# 确定存储策略
|
# 确定存储策略
|
||||||
policy_id = request.policy_id or parent.policy_id
|
policy_id = request.policy_id or parent.policy_id
|
||||||
policy = await Policy.get(session, Policy.id == policy_id)
|
policy = await Policy.get_exist_one(session, policy_id)
|
||||||
if not policy:
|
|
||||||
raise HTTPException(status_code=404, detail="存储策略不存在")
|
|
||||||
|
|
||||||
# 校验用户组是否有权使用该策略(仅当用户显式指定 policy_id 时)
|
# 校验用户组是否有权使用该策略(仅当用户显式指定 policy_id 时)
|
||||||
if request.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_id = request.policy_id or parent.policy_id
|
||||||
policy = await Policy.get(session, Policy.id == policy_id)
|
policy = await Policy.get_exist_one(session, policy_id)
|
||||||
if not policy:
|
|
||||||
raise HTTPException(status_code=404, detail="存储策略不存在")
|
|
||||||
|
|
||||||
# 生成存储路径并创建空文件
|
# 生成存储路径并创建空文件
|
||||||
storage_path: str | None = None
|
storage_path: str | None = None
|
||||||
@@ -942,7 +938,7 @@ async def file_get(
|
|||||||
|
|
||||||
# 递增下载次数
|
# 递增下载次数
|
||||||
link.downloads += 1
|
link.downloads += 1
|
||||||
await link.save(session)
|
link = await link.save(session)
|
||||||
|
|
||||||
if policy.type == PolicyType.LOCAL:
|
if policy.type == PolicyType.LOCAL:
|
||||||
storage_service = LocalStorageService(policy)
|
storage_service = LocalStorageService(policy)
|
||||||
@@ -996,7 +992,7 @@ async def file_source_redirect(
|
|||||||
|
|
||||||
# 递增下载次数
|
# 递增下载次数
|
||||||
link.downloads += 1
|
link.downloads += 1
|
||||||
await link.save(session)
|
link = await link.save(session)
|
||||||
|
|
||||||
if policy.type == PolicyType.LOCAL:
|
if policy.type == PolicyType.LOCAL:
|
||||||
storage_service = LocalStorageService(policy)
|
storage_service = LocalStorageService(policy)
|
||||||
|
|||||||
@@ -112,9 +112,7 @@ async def router_object_create(
|
|||||||
|
|
||||||
# 确定存储策略
|
# 确定存储策略
|
||||||
policy_id = request.policy_id or parent.policy_id
|
policy_id = request.policy_id or parent.policy_id
|
||||||
policy = await Policy.get(session, Policy.id == policy_id)
|
policy = await Policy.get_exist_one(session, policy_id)
|
||||||
if not policy:
|
|
||||||
raise HTTPException(status_code=404, detail="存储策略不存在")
|
|
||||||
|
|
||||||
parent_id = parent.id
|
parent_id = parent.id
|
||||||
|
|
||||||
@@ -149,7 +147,7 @@ async def router_object_create(
|
|||||||
owner_id=user_id,
|
owner_id=user_id,
|
||||||
policy_id=policy_id,
|
policy_id=policy_id,
|
||||||
)
|
)
|
||||||
await file_object.save(session)
|
file_object = await file_object.save(session)
|
||||||
|
|
||||||
l.info(f"创建空白文件: {request.name}")
|
l.info(f"创建空白文件: {request.name}")
|
||||||
|
|
||||||
@@ -474,7 +472,7 @@ async def router_object_rename(
|
|||||||
|
|
||||||
# 更新名称
|
# 更新名称
|
||||||
obj.name = new_name
|
obj.name = new_name
|
||||||
await obj.save(session)
|
obj = await obj.save(session)
|
||||||
|
|
||||||
l.info(f"用户 {user_id} 将对象 {obj.id} 重命名为 {new_name}")
|
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,
|
dest_policy_id=dest_policy_id,
|
||||||
object_id=obj_id,
|
object_id=obj_id,
|
||||||
)
|
)
|
||||||
await task_props.save(session)
|
task_props = await task_props.save(session)
|
||||||
|
|
||||||
if obj_is_file:
|
if obj_is_file:
|
||||||
# 文件:后台迁移
|
# 文件:后台迁移
|
||||||
@@ -698,7 +696,7 @@ async def router_object_switch_policy(
|
|||||||
# 目录:先更新目录自身的 policy_id
|
# 目录:先更新目录自身的 policy_id
|
||||||
obj = await Object.get(session, Object.id == obj_id)
|
obj = await Object.get(session, Object.id == obj_id)
|
||||||
obj.policy_id = dest_policy_id
|
obj.policy_id = dest_policy_id
|
||||||
await obj.save(session)
|
obj = await obj.save(session)
|
||||||
|
|
||||||
if request.is_migrate_existing:
|
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 = await Task.get(session, Task.id == task_id)
|
||||||
task.status = TaskStatus.COMPLETED
|
task.status = TaskStatus.COMPLETED
|
||||||
task.progress = 100
|
task.progress = 100
|
||||||
await task.save(session)
|
task = await task.save(session)
|
||||||
|
|
||||||
# 重新获取 task 以读取最新状态
|
# 重新获取 task 以读取最新状态
|
||||||
task = await Task.get(session, Task.id == task_id)
|
task = await Task.get(session, Task.id == task_id)
|
||||||
@@ -850,7 +848,7 @@ async def router_patch_object_metadata(
|
|||||||
)
|
)
|
||||||
if existing:
|
if existing:
|
||||||
existing.value = patch.value
|
existing.value = patch.value
|
||||||
await existing.save(session)
|
existing = await existing.save(session)
|
||||||
else:
|
else:
|
||||||
entry = ObjectMetadata(
|
entry = ObjectMetadata(
|
||||||
object_id=object_id,
|
object_id=object_id,
|
||||||
@@ -858,6 +856,6 @@ async def router_patch_object_metadata(
|
|||||||
value=patch.value,
|
value=patch.value,
|
||||||
is_public=True,
|
is_public=True,
|
||||||
)
|
)
|
||||||
await entry.save(session)
|
entry = await entry.save(session)
|
||||||
|
|
||||||
l.info(f"用户 {user.id} 更新了对象 {object_id} 的 {len(request.patches)} 条元数据")
|
l.info(f"用户 {user.id} 更新了对象 {object_id} 的 {len(request.patches)} 条元数据")
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ async def router_create_custom_property(
|
|||||||
options=request.options,
|
options=request.options,
|
||||||
default_value=request.default_value,
|
default_value=request.default_value,
|
||||||
)
|
)
|
||||||
await definition.save(session)
|
definition = await definition.save(session)
|
||||||
|
|
||||||
l.info(f"用户 {user.id} 创建了自定义属性: {request.name}")
|
l.info(f"用户 {user.id} 创建了自定义属性: {request.name}")
|
||||||
|
|
||||||
@@ -128,12 +128,7 @@ async def router_update_custom_property(
|
|||||||
- 404: 属性定义不存在
|
- 404: 属性定义不存在
|
||||||
- 403: 无权操作此属性
|
- 403: 无权操作此属性
|
||||||
"""
|
"""
|
||||||
definition = await CustomPropertyDefinition.get(
|
definition = await CustomPropertyDefinition.get_exist_one(session, id)
|
||||||
session,
|
|
||||||
CustomPropertyDefinition.id == id,
|
|
||||||
)
|
|
||||||
if not definition:
|
|
||||||
raise HTTPException(status_code=404, detail="自定义属性不存在")
|
|
||||||
|
|
||||||
if definition.owner_id != user.id:
|
if definition.owner_id != user.id:
|
||||||
raise HTTPException(status_code=403, detail="无权操作此属性")
|
raise HTTPException(status_code=403, detail="无权操作此属性")
|
||||||
@@ -163,12 +158,7 @@ async def router_delete_custom_property(
|
|||||||
- 404: 属性定义不存在
|
- 404: 属性定义不存在
|
||||||
- 403: 无权操作此属性
|
- 403: 无权操作此属性
|
||||||
"""
|
"""
|
||||||
definition = await CustomPropertyDefinition.get(
|
definition = await CustomPropertyDefinition.get_exist_one(session, id)
|
||||||
session,
|
|
||||||
CustomPropertyDefinition.id == id,
|
|
||||||
)
|
|
||||||
if not definition:
|
|
||||||
raise HTTPException(status_code=404, detail="自定义属性不存在")
|
|
||||||
|
|
||||||
if definition.owner_id != user.id:
|
if definition.owner_id != user.id:
|
||||||
raise HTTPException(status_code=403, detail="无权操作此属性")
|
raise HTTPException(status_code=403, detail="无权操作此属性")
|
||||||
|
|||||||
@@ -45,12 +45,7 @@ async def router_share_get(
|
|||||||
4. 返回分享详情(含文件树和分享者信息)
|
4. 返回分享详情(含文件树和分享者信息)
|
||||||
"""
|
"""
|
||||||
# 1. 查询分享(预加载 user 和 object)
|
# 1. 查询分享(预加载 user 和 object)
|
||||||
share = await Share.get(
|
share = await Share.get_exist_one(session, id, load=[Share.user, Share.object])
|
||||||
session, Share.id == id,
|
|
||||||
load=[Share.user, Share.object],
|
|
||||||
)
|
|
||||||
if not share:
|
|
||||||
http_exceptions.raise_not_found(detail="分享不存在或已被取消")
|
|
||||||
|
|
||||||
# 2. 检查过期
|
# 2. 检查过期
|
||||||
now = datetime.now()
|
now = datetime.now()
|
||||||
@@ -474,16 +469,29 @@ def router_share_update(id: str) -> ResponseBase:
|
|||||||
path='/{id}',
|
path='/{id}',
|
||||||
summary='删除分享',
|
summary='删除分享',
|
||||||
description='Delete a share by ID.',
|
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:
|
认证:需要 JWT token
|
||||||
id (str): The ID of the share to be deleted.
|
|
||||||
|
流程:
|
||||||
Returns:
|
1. 通过分享ID查找分享
|
||||||
ResponseBase: A model containing the response data for the deleted share.
|
2. 验证分享属于当前用户
|
||||||
|
3. 删除分享记录
|
||||||
"""
|
"""
|
||||||
http_exceptions.raise_not_implemented()
|
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}")
|
||||||
@@ -234,7 +234,7 @@ async def router_user_register(
|
|||||||
group_id=default_group.id,
|
group_id=default_group.id,
|
||||||
)
|
)
|
||||||
new_user_id = new_user.id
|
new_user_id = new_user.id
|
||||||
await new_user.save(session)
|
new_user = await new_user.save(session)
|
||||||
|
|
||||||
# 7. 创建 AuthIdentity
|
# 7. 创建 AuthIdentity
|
||||||
hashed_password = Password.hash(request.credential) if request.credential else None
|
hashed_password = Password.hash(request.credential) if request.credential else None
|
||||||
@@ -246,7 +246,7 @@ async def router_user_register(
|
|||||||
is_verified=False,
|
is_verified=False,
|
||||||
user_id=new_user_id,
|
user_id=new_user_id,
|
||||||
)
|
)
|
||||||
await identity.save(session)
|
identity = await identity.save(session)
|
||||||
|
|
||||||
# 8. 创建用户根目录(使用用户组关联的第一个存储策略)
|
# 8. 创建用户根目录(使用用户组关联的第一个存储策略)
|
||||||
await session.refresh(default_group, ['policies'])
|
await session.refresh(default_group, ['policies'])
|
||||||
@@ -494,9 +494,24 @@ async def router_user_storage(
|
|||||||
if not group:
|
if not group:
|
||||||
raise HTTPException(status_code=404, detail="用户组不存在")
|
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
|
used: int = user.storage
|
||||||
free: int = max(0, total - used)
|
free: int = max(0, total - used)
|
||||||
|
|
||||||
@@ -638,7 +653,7 @@ async def router_user_authn_finish(
|
|||||||
is_verified=True,
|
is_verified=True,
|
||||||
user_id=user.id,
|
user_id=user.id,
|
||||||
)
|
)
|
||||||
await identity.save(session)
|
identity = await identity.save(session)
|
||||||
|
|
||||||
return authn.to_detail_response()
|
return authn.to_detail_response()
|
||||||
|
|
||||||
|
|||||||
@@ -218,7 +218,7 @@ async def router_user_settings_avatar(
|
|||||||
|
|
||||||
# 更新用户头像字段
|
# 更新用户头像字段
|
||||||
user.avatar = "file"
|
user.avatar = "file"
|
||||||
await user.save(session)
|
user = await user.save(session)
|
||||||
|
|
||||||
|
|
||||||
@user_settings_router.put(
|
@user_settings_router.put(
|
||||||
@@ -252,7 +252,7 @@ async def router_user_settings_avatar_gravatar(
|
|||||||
await delete_avatar_files(session, user.id)
|
await delete_avatar_files(session, user.id)
|
||||||
|
|
||||||
user.avatar = "gravatar"
|
user.avatar = "gravatar"
|
||||||
await user.save(session)
|
user = await user.save(session)
|
||||||
|
|
||||||
|
|
||||||
@user_settings_router.delete(
|
@user_settings_router.delete(
|
||||||
@@ -279,7 +279,7 @@ async def router_user_settings_avatar_delete(
|
|||||||
await delete_avatar_files(session, user.id)
|
await delete_avatar_files(session, user.id)
|
||||||
|
|
||||||
user.avatar = "default"
|
user.avatar = "default"
|
||||||
await user.save(session)
|
user = await user.save(session)
|
||||||
|
|
||||||
|
|
||||||
@user_settings_router.patch(
|
@user_settings_router.patch(
|
||||||
@@ -321,7 +321,7 @@ async def router_user_settings_theme(
|
|||||||
user.color_error = request.theme_colors.error
|
user.color_error = request.theme_colors.error
|
||||||
user.color_neutral = request.theme_colors.neutral
|
user.color_neutral = request.theme_colors.neutral
|
||||||
|
|
||||||
await user.save(session)
|
user = await user.save(session)
|
||||||
|
|
||||||
|
|
||||||
@user_settings_router.patch(
|
@user_settings_router.patch(
|
||||||
@@ -358,7 +358,7 @@ async def router_user_settings_change_password(
|
|||||||
http_exceptions.raise_forbidden("当前密码错误")
|
http_exceptions.raise_forbidden("当前密码错误")
|
||||||
|
|
||||||
email_identity.credential = Password.hash(request.new_password)
|
email_identity.credential = Password.hash(request.new_password)
|
||||||
await email_identity.save(session)
|
email_identity = await email_identity.save(session)
|
||||||
|
|
||||||
|
|
||||||
@user_settings_router.patch(
|
@user_settings_router.patch(
|
||||||
@@ -392,7 +392,7 @@ async def router_user_settings_patch(
|
|||||||
http_exceptions.raise_bad_request(f"设置项 {option.value} 不允许为空")
|
http_exceptions.raise_bad_request(f"设置项 {option.value} 不允许为空")
|
||||||
|
|
||||||
setattr(user, option.value, value)
|
setattr(user, option.value, value)
|
||||||
await user.save(session)
|
user = await user.save(session)
|
||||||
|
|
||||||
|
|
||||||
@user_settings_router.get(
|
@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: dict = orjson.loads(email_identity.extra_data) if email_identity.extra_data else {}
|
||||||
extra["two_factor"] = secret
|
extra["two_factor"] = secret
|
||||||
email_identity.extra_data = orjson.dumps(extra).decode('utf-8')
|
email_identity.extra_data = orjson.dumps(extra).decode('utf-8')
|
||||||
await email_identity.save(session)
|
email_identity = await email_identity.save(session)
|
||||||
|
|
||||||
|
|
||||||
# ==================== 认证身份管理 ====================
|
# ==================== 认证身份管理 ====================
|
||||||
|
|||||||
@@ -79,9 +79,7 @@ async def set_default_viewer(
|
|||||||
|
|
||||||
if existing:
|
if existing:
|
||||||
existing.app_id = request.app_id
|
existing.app_id = request.app_id
|
||||||
existing = await existing.save(session)
|
existing = await existing.save(session, load=UserFileAppDefault.app)
|
||||||
# 重新加载 app 关系
|
|
||||||
await session.refresh(existing, attribute_names=["app"])
|
|
||||||
return existing.to_response()
|
return existing.to_response()
|
||||||
else:
|
else:
|
||||||
new_default = UserFileAppDefault(
|
new_default = UserFileAppDefault(
|
||||||
@@ -89,9 +87,7 @@ async def set_default_viewer(
|
|||||||
extension=normalized_ext,
|
extension=normalized_ext,
|
||||||
app_id=request.app_id,
|
app_id=request.app_id,
|
||||||
)
|
)
|
||||||
new_default = await new_default.save(session)
|
new_default = await new_default.save(session, load=UserFileAppDefault.app)
|
||||||
# 重新加载 app 关系
|
|
||||||
await session.refresh(new_default, attribute_names=["app"])
|
|
||||||
return new_default.to_response()
|
return new_default.to_response()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
|
||||||
@@ -175,8 +175,7 @@ async def _finalize_upload(
|
|||||||
obj = await Object.get(session, Object.id == object_id)
|
obj = await Object.get(session, Object.id == object_id)
|
||||||
if obj:
|
if obj:
|
||||||
obj.sqlmodel_update({'size': size, 'physical_file_id': pf.id})
|
obj.sqlmodel_update({'size': size, 'physical_file_id': pf.id})
|
||||||
session.add(obj)
|
obj = await obj.save(session)
|
||||||
await session.commit()
|
|
||||||
|
|
||||||
# 更新用户存储用量
|
# 更新用户存储用量
|
||||||
if size > 0:
|
if size > 0:
|
||||||
@@ -193,8 +192,7 @@ async def _move_object(
|
|||||||
obj = await Object.get(session, Object.id == object_id)
|
obj = await Object.get(session, Object.id == object_id)
|
||||||
if obj:
|
if obj:
|
||||||
obj.sqlmodel_update({'parent_id': new_parent_id, 'name': new_name})
|
obj.sqlmodel_update({'parent_id': new_parent_id, 'name': new_name})
|
||||||
session.add(obj)
|
obj = await obj.save(session)
|
||||||
await session.commit()
|
|
||||||
|
|
||||||
|
|
||||||
async def _copy_object_recursive(
|
async def _copy_object_recursive(
|
||||||
|
|||||||
@@ -147,7 +147,7 @@ async def migrate_single_file(
|
|||||||
# 4. 更新 Object
|
# 4. 更新 Object
|
||||||
obj.policy_id = dest_policy.id
|
obj.policy_id = dest_policy.id
|
||||||
obj.physical_file_id = new_physical.id
|
obj.physical_file_id = new_physical.id
|
||||||
await obj.save(session)
|
obj = await obj.save(session)
|
||||||
|
|
||||||
# 5. 旧 PhysicalFile 引用计数 -1
|
# 5. 旧 PhysicalFile 引用计数 -1
|
||||||
old_physical.decrement_reference()
|
old_physical.decrement_reference()
|
||||||
@@ -159,7 +159,7 @@ async def migrate_single_file(
|
|||||||
l.warning(f"删除源文件失败(不影响迁移结果): {old_physical.storage_path}: {e}")
|
l.warning(f"删除源文件失败(不影响迁移结果): {old_physical.storage_path}: {e}")
|
||||||
await PhysicalFile.delete(session, old_physical)
|
await PhysicalFile.delete(session, old_physical)
|
||||||
else:
|
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}")
|
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.status = TaskStatus.COMPLETED
|
||||||
task.progress = 100
|
task.progress = 100
|
||||||
await task.save(session)
|
task = await task.save(session)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
l.error(f"文件迁移任务失败: {obj.id}: {e}")
|
l.error(f"文件迁移任务失败: {obj.id}: {e}")
|
||||||
task.status = TaskStatus.ERROR
|
task.status = TaskStatus.ERROR
|
||||||
task.error = str(e)[:500]
|
task.error = str(e)[:500]
|
||||||
await task.save(session)
|
task = await task.save(session)
|
||||||
|
|
||||||
|
|
||||||
async def migrate_directory_files(
|
async def migrate_directory_files(
|
||||||
@@ -244,7 +244,7 @@ async def migrate_directory_files(
|
|||||||
# 更新所有子目录的 policy_id
|
# 更新所有子目录的 policy_id
|
||||||
for sub_folder in folders_to_update:
|
for sub_folder in folders_to_update:
|
||||||
sub_folder.policy_id = dest_policy.id
|
sub_folder.policy_id = dest_policy.id
|
||||||
await sub_folder.save(session)
|
sub_folder = await sub_folder.save(session)
|
||||||
|
|
||||||
# 完成任务
|
# 完成任务
|
||||||
if errors:
|
if errors:
|
||||||
@@ -254,7 +254,7 @@ async def migrate_directory_files(
|
|||||||
task.status = TaskStatus.COMPLETED
|
task.status = TaskStatus.COMPLETED
|
||||||
|
|
||||||
task.progress = 100
|
task.progress = 100
|
||||||
await task.save(session)
|
task = await task.save(session)
|
||||||
|
|
||||||
l.info(
|
l.info(
|
||||||
f"目录迁移完成: {folder.name} ({folder.id}), "
|
f"目录迁移完成: {folder.name} ({folder.id}), "
|
||||||
@@ -264,7 +264,7 @@ async def migrate_directory_files(
|
|||||||
l.error(f"目录迁移任务失败: {folder.id}: {e}")
|
l.error(f"目录迁移任务失败: {folder.id}: {e}")
|
||||||
task.status = TaskStatus.ERROR
|
task.status = TaskStatus.ERROR
|
||||||
task.error = str(e)[:500]
|
task.error = str(e)[:500]
|
||||||
await task.save(session)
|
task = await task.save(session)
|
||||||
|
|
||||||
|
|
||||||
async def _collect_objects_recursive(
|
async def _collect_objects_recursive(
|
||||||
|
|||||||
@@ -287,7 +287,7 @@ async def permanently_delete_objects(
|
|||||||
await PhysicalFile.delete(session, physical_file, commit=False)
|
await PhysicalFile.delete(session, physical_file, commit=False)
|
||||||
l.debug(f"物理文件记录已删除: {physical_file.storage_path}")
|
l.debug(f"物理文件记录已删除: {physical_file.storage_path}")
|
||||||
else:
|
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}")
|
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)
|
await PhysicalFile.delete(session, physical_file, commit=False)
|
||||||
l.debug(f"物理文件记录已删除: {physical_file.storage_path}")
|
l.debug(f"物理文件记录已删除: {physical_file.storage_path}")
|
||||||
else:
|
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}")
|
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)
|
physical_file = await PhysicalFile.get(session, PhysicalFile.id == src_physical_file_id)
|
||||||
if physical_file:
|
if physical_file:
|
||||||
physical_file.increment_reference()
|
physical_file.increment_reference()
|
||||||
await physical_file.save(session)
|
physical_file = await physical_file.save(session)
|
||||||
total_copied_size += src_size
|
total_copied_size += src_size
|
||||||
|
|
||||||
new_obj = await new_obj.save(session)
|
new_obj = await new_obj.save(session)
|
||||||
|
|||||||
@@ -192,7 +192,7 @@ async def _login_oauth(
|
|||||||
# 已绑定 → 更新 OAuth 信息并返回关联用户
|
# 已绑定 → 更新 OAuth 信息并返回关联用户
|
||||||
identity.display_name = nickname
|
identity.display_name = nickname
|
||||||
identity.avatar_url = avatar_url
|
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)
|
user: User = await User.get(session, User.id == identity.user_id, load=User.group)
|
||||||
if not user:
|
if not user:
|
||||||
@@ -254,7 +254,7 @@ async def _auto_register_oauth_user(
|
|||||||
is_verified=True,
|
is_verified=True,
|
||||||
user_id=new_user_id,
|
user_id=new_user_id,
|
||||||
)
|
)
|
||||||
await identity.save(session)
|
identity = await identity.save(session)
|
||||||
|
|
||||||
# 创建用户根目录
|
# 创建用户根目录
|
||||||
default_policy = await Policy.get(session, Policy.name == "本地存储")
|
default_policy = await Policy.get(session, Policy.name == "本地存储")
|
||||||
@@ -335,7 +335,7 @@ async def _login_passkey(
|
|||||||
|
|
||||||
# 更新签名计数
|
# 更新签名计数
|
||||||
authn.sign_count = verification.new_sign_count
|
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)
|
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:
|
if not identity.is_verified:
|
||||||
identity.is_verified = True
|
identity.is_verified = True
|
||||||
await identity.save(session)
|
identity = await identity.save(session)
|
||||||
|
|
||||||
return user
|
return user
|
||||||
|
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ if __name__ == "__main__":
|
|||||||
|
|
||||||
setup(
|
setup(
|
||||||
name="disknext-ee",
|
name="disknext-ee",
|
||||||
|
packages=[],
|
||||||
ext_modules=cythonize(
|
ext_modules=cythonize(
|
||||||
extensions,
|
extensions,
|
||||||
compiler_directives={'language_level': "3"},
|
compiler_directives={'language_level': "3"},
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ from .object import (
|
|||||||
ObjectResponse,
|
ObjectResponse,
|
||||||
ObjectSwitchPolicyRequest,
|
ObjectSwitchPolicyRequest,
|
||||||
ObjectType,
|
ObjectType,
|
||||||
|
FileCategory,
|
||||||
PolicyResponse,
|
PolicyResponse,
|
||||||
UploadChunkResponse,
|
UploadChunkResponse,
|
||||||
UploadSession,
|
UploadSession,
|
||||||
@@ -116,12 +117,22 @@ from .custom_property import (
|
|||||||
)
|
)
|
||||||
from .physical_file import PhysicalFile, PhysicalFileBase
|
from .physical_file import PhysicalFile, PhysicalFileBase
|
||||||
from .uri import DiskNextURI, FileSystemNamespace
|
from .uri import DiskNextURI, FileSystemNamespace
|
||||||
from .order import Order, OrderStatus, OrderType
|
from .order import (
|
||||||
|
Order, OrderStatus, OrderType,
|
||||||
|
CreateOrderRequest, OrderResponse,
|
||||||
|
)
|
||||||
from .policy import (
|
from .policy import (
|
||||||
Policy, PolicyBase, PolicyCreateRequest, PolicyOptions, PolicyOptionsBase,
|
Policy, PolicyBase, PolicyCreateRequest, PolicyOptions, PolicyOptionsBase,
|
||||||
PolicyType, PolicySummary, PolicyUpdateRequest,
|
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 .report import Report, ReportReason
|
||||||
from .setting import (
|
from .setting import (
|
||||||
Setting, SettingsType, SiteConfigResponse, AuthMethodConfig,
|
Setting, SettingsType, SiteConfigResponse, AuthMethodConfig,
|
||||||
@@ -134,7 +145,7 @@ from .share import (
|
|||||||
AdminShareListItem,
|
AdminShareListItem,
|
||||||
)
|
)
|
||||||
from .source_link import SourceLink
|
from .source_link import SourceLink
|
||||||
from .storage_pack import StoragePack
|
from .storage_pack import StoragePack, StoragePackResponse
|
||||||
from .tag import Tag, TagType
|
from .tag import Tag, TagType
|
||||||
from .task import Task, TaskProps, TaskPropsBase, TaskStatus, TaskType, TaskSummary, TaskSummaryBase
|
from .task import Task, TaskProps, TaskPropsBase, TaskStatus, TaskType, TaskSummary, TaskSummaryBase
|
||||||
from .webdav import (
|
from .webdav import (
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ from uuid import UUID
|
|||||||
|
|
||||||
from sqlmodel import Field, Relationship, UniqueConstraint
|
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:
|
if TYPE_CHECKING:
|
||||||
from .user import User
|
from .user import User
|
||||||
@@ -87,7 +87,7 @@ class ChangePasswordRequest(SQLModelBase):
|
|||||||
old_password: str = Field(min_length=1)
|
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 位)"""
|
"""新密码(至少 8 位)"""
|
||||||
|
|
||||||
|
|
||||||
@@ -103,13 +103,13 @@ class AuthIdentity(SQLModelBase, UUIDTableBaseMixin):
|
|||||||
provider: AuthProviderType = Field(index=True)
|
provider: AuthProviderType = Field(index=True)
|
||||||
"""提供者类型"""
|
"""提供者类型"""
|
||||||
|
|
||||||
identifier: str = Field(max_length=255, index=True)
|
identifier: Str255 = Field(index=True)
|
||||||
"""标识符(邮箱/手机号/OAuth openid)"""
|
"""标识符(邮箱/手机号/OAuth openid)"""
|
||||||
|
|
||||||
credential: str | None = Field(default=None, max_length=1024)
|
credential: Text1024 | None = None
|
||||||
"""凭证(Argon2 哈希密码 / null)"""
|
"""凭证(Argon2 哈希密码 / null)"""
|
||||||
|
|
||||||
display_name: str | None = Field(default=None, max_length=100)
|
display_name: Str100 | None = None
|
||||||
"""OAuth 昵称"""
|
"""OAuth 昵称"""
|
||||||
|
|
||||||
avatar_url: str | None = Field(default=None, max_length=512)
|
avatar_url: str | None = Field(default=None, max_length=512)
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ from uuid import UUID
|
|||||||
from sqlalchemy import JSON
|
from sqlalchemy import JSON
|
||||||
from sqlmodel import Field, Relationship
|
from sqlmodel import Field, Relationship
|
||||||
|
|
||||||
from sqlmodel_ext import SQLModelBase, UUIDTableBaseMixin
|
from sqlmodel_ext import SQLModelBase, UUIDTableBaseMixin, Str100
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .user import User
|
from .user import User
|
||||||
@@ -44,13 +44,13 @@ class CustomPropertyType(StrEnum):
|
|||||||
class CustomPropertyDefinitionBase(SQLModelBase):
|
class CustomPropertyDefinitionBase(SQLModelBase):
|
||||||
"""自定义属性定义基础模型"""
|
"""自定义属性定义基础模型"""
|
||||||
|
|
||||||
name: str = Field(max_length=100)
|
name: Str100
|
||||||
"""属性显示名称"""
|
"""属性显示名称"""
|
||||||
|
|
||||||
type: CustomPropertyType
|
type: CustomPropertyType
|
||||||
"""属性值类型"""
|
"""属性值类型"""
|
||||||
|
|
||||||
icon: str | None = Field(default=None, max_length=100)
|
icon: Str100 | None = None
|
||||||
"""图标标识(iconify 名称)"""
|
"""图标标识(iconify 名称)"""
|
||||||
|
|
||||||
options: list[str] | None = Field(default=None, sa_type=JSON)
|
options: list[str] | None = Field(default=None, sa_type=JSON)
|
||||||
@@ -90,7 +90,7 @@ class CustomPropertyDefinition(CustomPropertyDefinitionBase, UUIDTableBaseMixin)
|
|||||||
class CustomPropertyCreateRequest(SQLModelBase):
|
class CustomPropertyCreateRequest(SQLModelBase):
|
||||||
"""创建自定义属性请求 DTO"""
|
"""创建自定义属性请求 DTO"""
|
||||||
|
|
||||||
name: str = Field(max_length=100)
|
name: Str100
|
||||||
"""属性显示名称"""
|
"""属性显示名称"""
|
||||||
|
|
||||||
type: CustomPropertyType
|
type: CustomPropertyType
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from uuid import UUID
|
|||||||
|
|
||||||
from sqlmodel import Field, Relationship, UniqueConstraint, Index
|
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:
|
if TYPE_CHECKING:
|
||||||
from .user import User
|
from .user import User
|
||||||
@@ -141,7 +141,7 @@ class Download(DownloadBase, UUIDTableBaseMixin):
|
|||||||
speed: int = Field(default=0)
|
speed: int = Field(default=0)
|
||||||
"""下载速度(bytes/s)"""
|
"""下载速度(bytes/s)"""
|
||||||
|
|
||||||
parent: str | None = Field(default=None, max_length=255)
|
parent: Str255 | None = None
|
||||||
"""父任务标识"""
|
"""父任务标识"""
|
||||||
|
|
||||||
error: str | None = Field(default=None)
|
error: str | None = Field(default=None)
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ from uuid import UUID
|
|||||||
|
|
||||||
from sqlmodel import Field, Relationship, UniqueConstraint
|
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:
|
if TYPE_CHECKING:
|
||||||
from .group import Group
|
from .group import Group
|
||||||
@@ -119,7 +119,7 @@ class UserFileAppDefaultResponse(SQLModelBase):
|
|||||||
class FileAppCreateRequest(SQLModelBase):
|
class FileAppCreateRequest(SQLModelBase):
|
||||||
"""管理员创建应用请求 DTO"""
|
"""管理员创建应用请求 DTO"""
|
||||||
|
|
||||||
name: str = Field(max_length=100)
|
name: Str100
|
||||||
"""应用名称"""
|
"""应用名称"""
|
||||||
|
|
||||||
app_key: str = Field(max_length=50)
|
app_key: str = Field(max_length=50)
|
||||||
@@ -128,7 +128,7 @@ class FileAppCreateRequest(SQLModelBase):
|
|||||||
type: FileAppType
|
type: FileAppType
|
||||||
"""应用类型"""
|
"""应用类型"""
|
||||||
|
|
||||||
icon: str | None = Field(default=None, max_length=255)
|
icon: Str255 | None = None
|
||||||
"""图标名称/URL"""
|
"""图标名称/URL"""
|
||||||
|
|
||||||
description: str | None = Field(default=None, max_length=500)
|
description: str | None = Field(default=None, max_length=500)
|
||||||
@@ -140,13 +140,13 @@ class FileAppCreateRequest(SQLModelBase):
|
|||||||
is_restricted: bool = False
|
is_restricted: bool = False
|
||||||
"""是否限制用户组访问"""
|
"""是否限制用户组访问"""
|
||||||
|
|
||||||
iframe_url_template: str | None = Field(default=None, max_length=1024)
|
iframe_url_template: Text1024 | None = None
|
||||||
"""iframe URL 模板"""
|
"""iframe URL 模板"""
|
||||||
|
|
||||||
wopi_discovery_url: str | None = Field(default=None, max_length=512)
|
wopi_discovery_url: str | None = Field(default=None, max_length=512)
|
||||||
"""WOPI 发现端点 URL"""
|
"""WOPI 发现端点 URL"""
|
||||||
|
|
||||||
wopi_editor_url_template: str | None = Field(default=None, max_length=1024)
|
wopi_editor_url_template: Text1024 | None = None
|
||||||
"""WOPI 编辑器 URL 模板"""
|
"""WOPI 编辑器 URL 模板"""
|
||||||
|
|
||||||
extensions: list[str] = []
|
extensions: list[str] = []
|
||||||
@@ -159,7 +159,7 @@ class FileAppCreateRequest(SQLModelBase):
|
|||||||
class FileAppUpdateRequest(SQLModelBase):
|
class FileAppUpdateRequest(SQLModelBase):
|
||||||
"""管理员更新应用请求 DTO(所有字段可选)"""
|
"""管理员更新应用请求 DTO(所有字段可选)"""
|
||||||
|
|
||||||
name: str | None = Field(default=None, max_length=100)
|
name: Str100 | None = None
|
||||||
"""应用名称"""
|
"""应用名称"""
|
||||||
|
|
||||||
app_key: str | None = Field(default=None, max_length=50)
|
app_key: str | None = Field(default=None, max_length=50)
|
||||||
@@ -168,7 +168,7 @@ class FileAppUpdateRequest(SQLModelBase):
|
|||||||
type: FileAppType | None = None
|
type: FileAppType | None = None
|
||||||
"""应用类型"""
|
"""应用类型"""
|
||||||
|
|
||||||
icon: str | None = Field(default=None, max_length=255)
|
icon: Str255 | None = None
|
||||||
"""图标名称/URL"""
|
"""图标名称/URL"""
|
||||||
|
|
||||||
description: str | None = Field(default=None, max_length=500)
|
description: str | None = Field(default=None, max_length=500)
|
||||||
@@ -180,13 +180,13 @@ class FileAppUpdateRequest(SQLModelBase):
|
|||||||
is_restricted: bool | None = None
|
is_restricted: bool | None = None
|
||||||
"""是否限制用户组访问"""
|
"""是否限制用户组访问"""
|
||||||
|
|
||||||
iframe_url_template: str | None = Field(default=None, max_length=1024)
|
iframe_url_template: Text1024 | None = None
|
||||||
"""iframe URL 模板"""
|
"""iframe URL 模板"""
|
||||||
|
|
||||||
wopi_discovery_url: str | None = Field(default=None, max_length=512)
|
wopi_discovery_url: str | None = Field(default=None, max_length=512)
|
||||||
"""WOPI 发现端点 URL"""
|
"""WOPI 发现端点 URL"""
|
||||||
|
|
||||||
wopi_editor_url_template: str | None = Field(default=None, max_length=1024)
|
wopi_editor_url_template: Text1024 | None = None
|
||||||
"""WOPI 编辑器 URL 模板"""
|
"""WOPI 编辑器 URL 模板"""
|
||||||
|
|
||||||
|
|
||||||
@@ -325,7 +325,7 @@ class WopiDiscoveryResponse(SQLModelBase):
|
|||||||
class FileApp(SQLModelBase, UUIDTableBaseMixin):
|
class FileApp(SQLModelBase, UUIDTableBaseMixin):
|
||||||
"""文件查看器应用注册表"""
|
"""文件查看器应用注册表"""
|
||||||
|
|
||||||
name: str = Field(max_length=100)
|
name: Str100
|
||||||
"""应用名称"""
|
"""应用名称"""
|
||||||
|
|
||||||
app_key: str = Field(max_length=50, unique=True, index=True)
|
app_key: str = Field(max_length=50, unique=True, index=True)
|
||||||
@@ -334,7 +334,7 @@ class FileApp(SQLModelBase, UUIDTableBaseMixin):
|
|||||||
type: FileAppType
|
type: FileAppType
|
||||||
"""应用类型"""
|
"""应用类型"""
|
||||||
|
|
||||||
icon: str | None = Field(default=None, max_length=255)
|
icon: Str255 | None = None
|
||||||
"""图标名称/URL"""
|
"""图标名称/URL"""
|
||||||
|
|
||||||
description: str | None = Field(default=None, max_length=500)
|
description: str | None = Field(default=None, max_length=500)
|
||||||
@@ -346,13 +346,13 @@ class FileApp(SQLModelBase, UUIDTableBaseMixin):
|
|||||||
is_restricted: bool = False
|
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} 占位符"""
|
"""iframe URL 模板,支持 {file_url} 占位符"""
|
||||||
|
|
||||||
wopi_discovery_url: str | None = Field(default=None, max_length=512)
|
wopi_discovery_url: str | None = Field(default=None, max_length=512)
|
||||||
"""WOPI 客户端发现端点 URL"""
|
"""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}"""
|
"""WOPI 编辑器 URL 模板,支持 {wopi_src} {access_token} {access_token_ttl}"""
|
||||||
|
|
||||||
# 关系
|
# 关系
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from uuid import UUID
|
|||||||
from sqlalchemy import BigInteger
|
from sqlalchemy import BigInteger
|
||||||
from sqlmodel import Field, Relationship, text
|
from sqlmodel import Field, Relationship, text
|
||||||
|
|
||||||
from sqlmodel_ext import SQLModelBase, TableBaseMixin, UUIDTableBaseMixin
|
from sqlmodel_ext import SQLModelBase, TableBaseMixin, UUIDTableBaseMixin, Str255
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .user import User
|
from .user import User
|
||||||
@@ -67,7 +67,7 @@ class GroupAllOptionsBase(GroupOptionsBase):
|
|||||||
class GroupCreateRequest(GroupAllOptionsBase):
|
class GroupCreateRequest(GroupAllOptionsBase):
|
||||||
"""创建用户组请求 DTO"""
|
"""创建用户组请求 DTO"""
|
||||||
|
|
||||||
name: str = Field(max_length=255)
|
name: Str255
|
||||||
"""用户组名称"""
|
"""用户组名称"""
|
||||||
|
|
||||||
max_storage: int = Field(default=0, ge=0)
|
max_storage: int = Field(default=0, ge=0)
|
||||||
@@ -92,7 +92,7 @@ class GroupCreateRequest(GroupAllOptionsBase):
|
|||||||
class GroupUpdateRequest(SQLModelBase):
|
class GroupUpdateRequest(SQLModelBase):
|
||||||
"""更新用户组请求 DTO(所有字段可选)"""
|
"""更新用户组请求 DTO(所有字段可选)"""
|
||||||
|
|
||||||
name: str | None = Field(default=None, max_length=255)
|
name: Str255 | None = None
|
||||||
"""用户组名称"""
|
"""用户组名称"""
|
||||||
|
|
||||||
max_storage: int | None = Field(default=None, ge=0)
|
max_storage: int | None = Field(default=None, ge=0)
|
||||||
@@ -258,7 +258,7 @@ class GroupOptions(GroupAllOptionsBase, TableBaseMixin):
|
|||||||
class Group(GroupBase, UUIDTableBaseMixin):
|
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"})
|
max_storage: int = Field(default=0, sa_type=BigInteger, sa_column_kwargs={"server_default": "0"})
|
||||||
|
|||||||
@@ -130,6 +130,11 @@ default_settings: list[Setting] = [
|
|||||||
Setting(name="sms_provider", value="", type=SettingsType.MOBILE),
|
Setting(name="sms_provider", value="", type=SettingsType.MOBILE),
|
||||||
Setting(name="sms_access_key", value="", type=SettingsType.MOBILE),
|
Setting(name="sms_access_key", value="", type=SettingsType.MOBILE),
|
||||||
Setting(name="sms_secret_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:
|
async def init_default_settings() -> None:
|
||||||
@@ -173,7 +178,7 @@ async def init_default_group() -> None:
|
|||||||
admin=True,
|
admin=True,
|
||||||
)
|
)
|
||||||
admin_group_id = admin_group.id # 在 save 前保存 UUID
|
admin_group_id = admin_group.id # 在 save 前保存 UUID
|
||||||
await admin_group.save(session)
|
admin_group = await admin_group.save(session)
|
||||||
|
|
||||||
await GroupOptions(
|
await GroupOptions(
|
||||||
group_id=admin_group_id,
|
group_id=admin_group_id,
|
||||||
@@ -203,7 +208,7 @@ async def init_default_group() -> None:
|
|||||||
web_dav_enabled=True,
|
web_dav_enabled=True,
|
||||||
)
|
)
|
||||||
member_group_id = member_group.id # 在 save 前保存 UUID
|
member_group_id = member_group.id # 在 save 前保存 UUID
|
||||||
await member_group.save(session)
|
member_group = await member_group.save(session)
|
||||||
|
|
||||||
await GroupOptions(
|
await GroupOptions(
|
||||||
group_id=member_group_id,
|
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")
|
default_group_setting = await Setting.get(session, Setting.name == "default_group")
|
||||||
if default_group_setting:
|
if default_group_setting:
|
||||||
default_group_setting.value = str(member_group_id)
|
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 == "游客"):
|
if not await Group.get(session, Group.name == "游客"):
|
||||||
@@ -232,7 +237,7 @@ async def init_default_group() -> None:
|
|||||||
web_dav_enabled=False,
|
web_dav_enabled=False,
|
||||||
)
|
)
|
||||||
guest_group_id = guest_group.id # 在 save 前保存 UUID
|
guest_group_id = guest_group.id # 在 save 前保存 UUID
|
||||||
await guest_group.save(session)
|
guest_group = await guest_group.save(session)
|
||||||
|
|
||||||
await GroupOptions(
|
await GroupOptions(
|
||||||
group_id=guest_group_id,
|
group_id=guest_group_id,
|
||||||
@@ -284,7 +289,7 @@ async def init_default_user() -> None:
|
|||||||
group_id=admin_group.id,
|
group_id=admin_group.id,
|
||||||
)
|
)
|
||||||
admin_user_id = admin_user.id # 在 save 前保存 UUID
|
admin_user_id = admin_user.id # 在 save 前保存 UUID
|
||||||
await admin_user.save(session)
|
admin_user = await admin_user.save(session)
|
||||||
|
|
||||||
# 创建 AuthIdentity(邮箱密码身份)
|
# 创建 AuthIdentity(邮箱密码身份)
|
||||||
await AuthIdentity(
|
await AuthIdentity(
|
||||||
@@ -373,7 +378,7 @@ async def init_default_theme_presets() -> None:
|
|||||||
error=ChromaticColor.RED,
|
error=ChromaticColor.RED,
|
||||||
neutral=NeutralColor.ZINC,
|
neutral=NeutralColor.ZINC,
|
||||||
)
|
)
|
||||||
await default_preset.save(session)
|
default_preset = await default_preset.save(session)
|
||||||
log.info('已创建默认主题预设')
|
log.info('已创建默认主题预设')
|
||||||
|
|
||||||
|
|
||||||
@@ -522,6 +527,6 @@ async def init_default_file_apps() -> None:
|
|||||||
extension=ext.lower(),
|
extension=ext.lower(),
|
||||||
priority=i,
|
priority=i,
|
||||||
)
|
)
|
||||||
await ext_record.save(session)
|
ext_record = await ext_record.save(session)
|
||||||
|
|
||||||
log.info(f'已创建 {len(_DEFAULT_FILE_APPS)} 个默认文件查看器应用')
|
log.info(f'已创建 {len(_DEFAULT_FILE_APPS)} 个默认文件查看器应用')
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from typing import TYPE_CHECKING
|
|||||||
|
|
||||||
from sqlmodel import Field, Relationship, text, Index
|
from sqlmodel import Field, Relationship, text, Index
|
||||||
|
|
||||||
from sqlmodel_ext import SQLModelBase, TableBaseMixin
|
from sqlmodel_ext import SQLModelBase, TableBaseMixin, Str255
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .download import Download
|
from .download import Download
|
||||||
@@ -28,13 +28,13 @@ class NodeType(StrEnum):
|
|||||||
class Aria2ConfigurationBase(SQLModelBase):
|
class Aria2ConfigurationBase(SQLModelBase):
|
||||||
"""Aria2配置基础模型"""
|
"""Aria2配置基础模型"""
|
||||||
|
|
||||||
rpc_url: str | None = Field(default=None, max_length=255)
|
rpc_url: Str255 | None = None
|
||||||
"""RPC地址"""
|
"""RPC地址"""
|
||||||
|
|
||||||
rpc_secret: str | None = None
|
rpc_secret: str | None = None
|
||||||
"""RPC密钥"""
|
"""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)
|
max_concurrent: int = Field(default=5, ge=1, le=50)
|
||||||
@@ -70,19 +70,19 @@ class Node(SQLModelBase, TableBaseMixin):
|
|||||||
status: NodeStatus = Field(default=NodeStatus.ONLINE)
|
status: NodeStatus = Field(default=NodeStatus.ONLINE)
|
||||||
"""节点状态"""
|
"""节点状态"""
|
||||||
|
|
||||||
name: str = Field(max_length=255, unique=True)
|
name: Str255 = Field(unique=True)
|
||||||
"""节点名称"""
|
"""节点名称"""
|
||||||
|
|
||||||
type: NodeType
|
type: NodeType
|
||||||
"""节点类型"""
|
"""节点类型"""
|
||||||
|
|
||||||
server: str = Field(max_length=255)
|
server: Str255
|
||||||
"""节点地址(IP或域名)"""
|
"""节点地址(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
|
aria2_enabled: bool = False
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from enum import StrEnum
|
|||||||
from sqlalchemy import BigInteger
|
from sqlalchemy import BigInteger
|
||||||
from sqlmodel import Field, Relationship, CheckConstraint, Index, text
|
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
|
from .policy import PolicyType
|
||||||
|
|
||||||
@@ -25,7 +25,15 @@ class ObjectType(StrEnum):
|
|||||||
"""对象类型枚举"""
|
"""对象类型枚举"""
|
||||||
FILE = "file"
|
FILE = "file"
|
||||||
FOLDER = "folder"
|
FOLDER = "folder"
|
||||||
|
|
||||||
|
|
||||||
|
class FileCategory(StrEnum):
|
||||||
|
"""文件类型分类枚举,用于按类别筛选文件"""
|
||||||
|
IMAGE = "image"
|
||||||
|
VIDEO = "video"
|
||||||
|
AUDIO = "audio"
|
||||||
|
DOCUMENT = "document"
|
||||||
|
|
||||||
|
|
||||||
# ==================== Base 模型 ====================
|
# ==================== Base 模型 ====================
|
||||||
|
|
||||||
@@ -190,13 +198,13 @@ class Object(ObjectBase, UUIDTableBaseMixin):
|
|||||||
|
|
||||||
# ==================== 基础字段 ====================
|
# ==================== 基础字段 ====================
|
||||||
|
|
||||||
name: str = Field(max_length=255)
|
name: Str255
|
||||||
"""对象名称(文件名或目录名)"""
|
"""对象名称(文件名或目录名)"""
|
||||||
|
|
||||||
type: ObjectType
|
type: ObjectType
|
||||||
"""对象类型:file 或 folder"""
|
"""对象类型: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"})
|
size: int = Field(default=0, sa_type=BigInteger, sa_column_kwargs={"server_default": "0"})
|
||||||
"""文件大小(字节),目录为 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(仅文件有效)"""
|
"""分块上传会话ID(仅文件有效)"""
|
||||||
|
|
||||||
physical_file_id: UUID | None = Field(
|
physical_file_id: UUID | None = Field(
|
||||||
@@ -469,6 +477,37 @@ class Object(ObjectBase, UUIDTableBaseMixin):
|
|||||||
fetch_mode="all"
|
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
|
@classmethod
|
||||||
async def resolve_uri(
|
async def resolve_uri(
|
||||||
cls,
|
cls,
|
||||||
@@ -546,7 +585,7 @@ class Object(ObjectBase, UUIDTableBaseMixin):
|
|||||||
class UploadSessionBase(SQLModelBase):
|
class UploadSessionBase(SQLModelBase):
|
||||||
"""上传会话基础字段"""
|
"""上传会话基础字段"""
|
||||||
|
|
||||||
file_name: str = Field(max_length=255)
|
file_name: Str255
|
||||||
"""原始文件名"""
|
"""原始文件名"""
|
||||||
|
|
||||||
file_size: int = Field(ge=0, sa_type=BigInteger)
|
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)
|
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 Multipart Upload ID(仅 S3 策略使用)"""
|
||||||
|
|
||||||
s3_part_etags: str | None = None
|
s3_part_etags: str | None = None
|
||||||
@@ -624,7 +663,7 @@ class UploadSession(UploadSessionBase, UUIDTableBaseMixin):
|
|||||||
class CreateUploadSessionRequest(SQLModelBase):
|
class CreateUploadSessionRequest(SQLModelBase):
|
||||||
"""创建上传会话请求 DTO"""
|
"""创建上传会话请求 DTO"""
|
||||||
|
|
||||||
file_name: str = Field(max_length=255)
|
file_name: Str255
|
||||||
"""文件名"""
|
"""文件名"""
|
||||||
|
|
||||||
file_size: int = Field(ge=0)
|
file_size: int = Field(ge=0)
|
||||||
@@ -681,7 +720,7 @@ class UploadChunkResponse(SQLModelBase):
|
|||||||
class CreateFileRequest(SQLModelBase):
|
class CreateFileRequest(SQLModelBase):
|
||||||
"""创建空白文件请求 DTO"""
|
"""创建空白文件请求 DTO"""
|
||||||
|
|
||||||
name: str = Field(max_length=255)
|
name: Str255
|
||||||
"""文件名"""
|
"""文件名"""
|
||||||
|
|
||||||
parent_id: UUID
|
parent_id: UUID
|
||||||
@@ -719,7 +758,7 @@ class ObjectRenameRequest(SQLModelBase):
|
|||||||
id: UUID
|
id: UUID
|
||||||
"""对象UUID"""
|
"""对象UUID"""
|
||||||
|
|
||||||
new_name: str = Field(max_length=255)
|
new_name: Str255
|
||||||
"""新名称"""
|
"""新名称"""
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ from uuid import UUID
|
|||||||
|
|
||||||
from sqlmodel import Field, UniqueConstraint, Index, Relationship
|
from sqlmodel import Field, UniqueConstraint, Index, Relationship
|
||||||
|
|
||||||
from sqlmodel_ext import SQLModelBase, UUIDTableBaseMixin
|
from sqlmodel_ext import SQLModelBase, UUIDTableBaseMixin, Str255
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .object import Object
|
from .object import Object
|
||||||
@@ -65,7 +65,7 @@ USER_WRITABLE_NAMESPACES: set[str] = {MetadataNamespace.CUSTOM}
|
|||||||
class ObjectMetadataBase(SQLModelBase):
|
class ObjectMetadataBase(SQLModelBase):
|
||||||
"""对象元数据 KV 基础模型"""
|
"""对象元数据 KV 基础模型"""
|
||||||
|
|
||||||
name: str = Field(max_length=255)
|
name: Str255
|
||||||
"""元数据键名,格式:namespace:key(如 exif:width, stream:duration)"""
|
"""元数据键名,格式:namespace:key(如 exif:width, stream:duration)"""
|
||||||
|
|
||||||
value: str
|
value: str
|
||||||
@@ -113,7 +113,7 @@ class MetadataResponse(SQLModelBase):
|
|||||||
class MetadataPatchItem(SQLModelBase):
|
class MetadataPatchItem(SQLModelBase):
|
||||||
"""单条元数据补丁 DTO"""
|
"""单条元数据补丁 DTO"""
|
||||||
|
|
||||||
key: str = Field(max_length=255)
|
key: Str255
|
||||||
"""元数据键名"""
|
"""元数据键名"""
|
||||||
|
|
||||||
value: str | None = None
|
value: str | None = None
|
||||||
|
|||||||
@@ -1,58 +1,122 @@
|
|||||||
|
from decimal import Decimal
|
||||||
from enum import StrEnum
|
from enum import StrEnum
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
|
from sqlalchemy import Numeric
|
||||||
from sqlmodel import Field, Relationship
|
from sqlmodel import Field, Relationship
|
||||||
|
|
||||||
from sqlmodel_ext import SQLModelBase, TableBaseMixin
|
from sqlmodel_ext import SQLModelBase, TableBaseMixin, Str255
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
from .product import Product
|
||||||
from .user import User
|
from .user import User
|
||||||
|
|
||||||
|
|
||||||
class OrderStatus(StrEnum):
|
class OrderStatus(StrEnum):
|
||||||
"""订单状态枚举"""
|
"""订单状态枚举"""
|
||||||
|
|
||||||
PENDING = "pending"
|
PENDING = "pending"
|
||||||
"""待支付"""
|
"""待支付"""
|
||||||
|
|
||||||
COMPLETED = "completed"
|
COMPLETED = "completed"
|
||||||
"""已完成"""
|
"""已完成"""
|
||||||
|
|
||||||
CANCELLED = "cancelled"
|
CANCELLED = "cancelled"
|
||||||
"""已取消"""
|
"""已取消"""
|
||||||
|
|
||||||
|
|
||||||
class OrderType(StrEnum):
|
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):
|
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"})
|
type: OrderType
|
||||||
"""订单类型 [TODO] 待定义枚举"""
|
"""订单类型"""
|
||||||
|
|
||||||
method: str | None = Field(default=None, max_length=255)
|
method: Str255 | None = None
|
||||||
"""支付方式"""
|
"""支付方式"""
|
||||||
|
|
||||||
product_id: int | None = Field(default=None)
|
product_id: UUID | None = Field(default=None, foreign_key="product.id", ondelete="SET NULL")
|
||||||
"""商品ID"""
|
"""关联商品UUID"""
|
||||||
|
|
||||||
num: int = Field(default=1, sa_column_kwargs={"server_default": "1"})
|
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)
|
status: OrderStatus = Field(default=OrderStatus.PENDING)
|
||||||
"""订单状态"""
|
"""订单状态"""
|
||||||
|
|
||||||
# 外键
|
# 外键
|
||||||
user_id: UUID = Field(
|
user_id: UUID = Field(
|
||||||
foreign_key="user.id",
|
foreign_key="user.id",
|
||||||
@@ -60,6 +124,22 @@ class Order(SQLModelBase, TableBaseMixin):
|
|||||||
ondelete="CASCADE"
|
ondelete="CASCADE"
|
||||||
)
|
)
|
||||||
"""所属用户UUID"""
|
"""所属用户UUID"""
|
||||||
|
|
||||||
# 关系
|
# 关系
|
||||||
user: "User" = Relationship(back_populates="orders")
|
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,
|
||||||
|
)
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ from uuid import UUID
|
|||||||
from sqlalchemy import BigInteger
|
from sqlalchemy import BigInteger
|
||||||
from sqlmodel import Field, Relationship, Index
|
from sqlmodel import Field, Relationship, Index
|
||||||
|
|
||||||
from sqlmodel_ext import SQLModelBase, UUIDTableBaseMixin
|
from sqlmodel_ext import SQLModelBase, UUIDTableBaseMixin, Str32, Str64
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .object import Object
|
from .object import Object
|
||||||
@@ -31,10 +31,10 @@ class PhysicalFileBase(SQLModelBase):
|
|||||||
size: int = Field(default=0, sa_type=BigInteger)
|
size: int = Field(default=0, sa_type=BigInteger)
|
||||||
"""文件大小(字节)"""
|
"""文件大小(字节)"""
|
||||||
|
|
||||||
checksum_md5: str | None = Field(default=None, max_length=32)
|
checksum_md5: Str32 | None = None
|
||||||
"""MD5校验和(用于文件去重和完整性校验)"""
|
"""MD5校验和(用于文件去重和完整性校验)"""
|
||||||
|
|
||||||
checksum_sha256: str | None = Field(default=None, max_length=64)
|
checksum_sha256: Str64 | None = None
|
||||||
"""SHA256校验和"""
|
"""SHA256校验和"""
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from uuid import UUID
|
|||||||
from enum import StrEnum
|
from enum import StrEnum
|
||||||
from sqlmodel import Field, Relationship, text
|
from sqlmodel import Field, Relationship, text
|
||||||
|
|
||||||
from sqlmodel_ext import SQLModelBase, UUIDTableBaseMixin
|
from sqlmodel_ext import SQLModelBase, UUIDTableBaseMixin, Str64, Str255
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .object import Object
|
from .object import Object
|
||||||
@@ -37,22 +37,22 @@ class PolicyType(StrEnum):
|
|||||||
class PolicyBase(SQLModelBase):
|
class PolicyBase(SQLModelBase):
|
||||||
"""存储策略基础字段,供 DTO 和数据库模型共享"""
|
"""存储策略基础字段,供 DTO 和数据库模型共享"""
|
||||||
|
|
||||||
name: str = Field(max_length=255)
|
name: Str255
|
||||||
"""策略名称"""
|
"""策略名称"""
|
||||||
|
|
||||||
type: PolicyType
|
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
|
is_private: bool = True
|
||||||
"""是否为私有空间"""
|
"""是否为私有空间"""
|
||||||
|
|
||||||
base_url: str | None = Field(default=None, max_length=255)
|
base_url: Str255 | None = None
|
||||||
"""访问文件的基础URL"""
|
"""访问文件的基础URL"""
|
||||||
|
|
||||||
access_key: str | None = None
|
access_key: str | None = None
|
||||||
@@ -67,10 +67,10 @@ class PolicyBase(SQLModelBase):
|
|||||||
auto_rename: bool = False
|
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
|
is_origin_link_enable: bool = False
|
||||||
@@ -115,7 +115,7 @@ class PolicyCreateRequest(PolicyBase):
|
|||||||
mimetype: str | None = Field(default=None, max_length=127)
|
mimetype: str | None = Field(default=None, max_length=127)
|
||||||
"""MIME类型"""
|
"""MIME类型"""
|
||||||
|
|
||||||
od_redirect: str | None = Field(default=None, max_length=255)
|
od_redirect: Str255 | None = None
|
||||||
"""OneDrive重定向地址"""
|
"""OneDrive重定向地址"""
|
||||||
|
|
||||||
chunk_size: int = Field(default=52428800, ge=1)
|
chunk_size: int = Field(default=52428800, ge=1)
|
||||||
@@ -124,26 +124,26 @@ class PolicyCreateRequest(PolicyBase):
|
|||||||
s3_path_style: bool = False
|
s3_path_style: bool = False
|
||||||
"""是否使用S3路径风格"""
|
"""是否使用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 策略使用"""
|
"""S3 区域(如 us-east-1、ap-southeast-1),仅 S3 策略使用"""
|
||||||
|
|
||||||
|
|
||||||
class PolicyUpdateRequest(SQLModelBase):
|
class PolicyUpdateRequest(SQLModelBase):
|
||||||
"""更新存储策略请求 DTO(所有字段可选)"""
|
"""更新存储策略请求 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
|
is_private: bool | None = None
|
||||||
"""是否为私有空间"""
|
"""是否为私有空间"""
|
||||||
|
|
||||||
base_url: str | None = Field(default=None, max_length=255)
|
base_url: Str255 | None = None
|
||||||
"""访问文件的基础URL"""
|
"""访问文件的基础URL"""
|
||||||
|
|
||||||
access_key: str | None = None
|
access_key: str | None = None
|
||||||
@@ -158,10 +158,10 @@ class PolicyUpdateRequest(SQLModelBase):
|
|||||||
auto_rename: bool | None = None
|
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
|
is_origin_link_enable: bool | None = None
|
||||||
@@ -177,7 +177,7 @@ class PolicyUpdateRequest(SQLModelBase):
|
|||||||
mimetype: str | None = Field(default=None, max_length=127)
|
mimetype: str | None = Field(default=None, max_length=127)
|
||||||
"""MIME类型"""
|
"""MIME类型"""
|
||||||
|
|
||||||
od_redirect: str | None = Field(default=None, max_length=255)
|
od_redirect: Str255 | None = None
|
||||||
"""OneDrive重定向地址"""
|
"""OneDrive重定向地址"""
|
||||||
|
|
||||||
chunk_size: int | None = Field(default=None, ge=1)
|
chunk_size: int | None = Field(default=None, ge=1)
|
||||||
@@ -186,7 +186,7 @@ class PolicyUpdateRequest(SQLModelBase):
|
|||||||
s3_path_style: bool | None = None
|
s3_path_style: bool | None = None
|
||||||
"""是否使用S3路径风格"""
|
"""是否使用S3路径风格"""
|
||||||
|
|
||||||
s3_region: str | None = Field(default=None, max_length=64)
|
s3_region: Str64 | None = None
|
||||||
"""S3 区域"""
|
"""S3 区域"""
|
||||||
|
|
||||||
|
|
||||||
@@ -205,7 +205,7 @@ class PolicyOptionsBase(SQLModelBase):
|
|||||||
mimetype: str | None = Field(default=None, max_length=127)
|
mimetype: str | None = Field(default=None, max_length=127)
|
||||||
"""MIME类型"""
|
"""MIME类型"""
|
||||||
|
|
||||||
od_redirect: str | None = Field(default=None, max_length=255)
|
od_redirect: Str255 | None = None
|
||||||
"""OneDrive重定向地址"""
|
"""OneDrive重定向地址"""
|
||||||
|
|
||||||
chunk_size: int = Field(default=52428800, sa_column_kwargs={"server_default": "52428800"})
|
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_path_style: bool = Field(default=False, sa_column_kwargs={"server_default": text("false")})
|
||||||
"""是否使用S3路径风格"""
|
"""是否使用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 策略使用"""
|
"""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")})
|
is_private: bool = Field(default=True, sa_column_kwargs={"server_default": text("true")})
|
||||||
|
|||||||
206
sqlmodels/product.py
Normal file
206
sqlmodels/product.py
Normal file
@@ -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,
|
||||||
|
)
|
||||||
@@ -1,22 +1,141 @@
|
|||||||
|
from datetime import datetime
|
||||||
from enum import StrEnum
|
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
|
from sqlmodel_ext import SQLModelBase, TableBaseMixin
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .product import Product
|
||||||
|
from .user import User
|
||||||
|
|
||||||
|
|
||||||
class RedeemType(StrEnum):
|
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):
|
class Redeem(SQLModelBase, TableBaseMixin):
|
||||||
"""兑换码模型"""
|
"""兑换码模型"""
|
||||||
|
|
||||||
type: int = Field(default=0, sa_column_kwargs={"server_default": "0"})
|
type: RedeemType
|
||||||
"""兑换码类型 [TODO] 待定义枚举"""
|
"""兑换码类型"""
|
||||||
product_id: int | None = Field(default=None, description="关联的商品/权益ID")
|
|
||||||
num: int = Field(default=1, sa_column_kwargs={"server_default": "1"}, description="可兑换数量/时长等")
|
product_id: UUID | None = Field(default=None, foreign_key="product.id", ondelete="SET NULL")
|
||||||
code: str = Field(unique=True, index=True, description="兑换码,唯一")
|
"""关联商品UUID"""
|
||||||
used: bool = Field(default=False, sa_column_kwargs={"server_default": text("false")}, description="是否已使用")
|
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from uuid import UUID
|
|||||||
|
|
||||||
from sqlmodel import Field, Relationship
|
from sqlmodel import Field, Relationship
|
||||||
|
|
||||||
from sqlmodel_ext import SQLModelBase, TableBaseMixin
|
from sqlmodel_ext import SQLModelBase, TableBaseMixin, Str255
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .share import Share
|
from .share import Share
|
||||||
@@ -21,7 +21,7 @@ class Report(SQLModelBase, TableBaseMixin):
|
|||||||
|
|
||||||
reason: int = Field(default=0, sa_column_kwargs={"server_default": "0"})
|
reason: int = Field(default=0, sa_column_kwargs={"server_default": "0"})
|
||||||
"""举报原因 [TODO] 待定义枚举"""
|
"""举报原因 [TODO] 待定义枚举"""
|
||||||
description: str | None = Field(default=None, max_length=255, description="补充描述")
|
description: Str255 | None = Field(default=None, description="补充描述")
|
||||||
|
|
||||||
# 外键
|
# 外键
|
||||||
share_id: UUID = Field(
|
share_id: UUID = Field(
|
||||||
|
|||||||
@@ -163,6 +163,7 @@ class SettingsType(StrEnum):
|
|||||||
VERSION = "version"
|
VERSION = "version"
|
||||||
VIEW = "view"
|
VIEW = "view"
|
||||||
WOPI = "wopi"
|
WOPI = "wopi"
|
||||||
|
FILE_CATEGORY = "file_category"
|
||||||
|
|
||||||
# 数据库模型
|
# 数据库模型
|
||||||
class Setting(SettingItem, TableBaseMixin):
|
class Setting(SettingItem, TableBaseMixin):
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from uuid import UUID
|
|||||||
|
|
||||||
from sqlmodel import Field, Relationship, text, UniqueConstraint, Index
|
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 .model_base import ResponseBase
|
||||||
from .object import ObjectType
|
from .object import ObjectType
|
||||||
@@ -52,10 +52,10 @@ class Share(SQLModelBase, UUIDTableBaseMixin):
|
|||||||
Index("ix_share_object", "object_id"),
|
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(
|
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")})
|
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)
|
score: int = Field(default=0, ge=0)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from uuid import UUID
|
|||||||
|
|
||||||
from sqlmodel import Field, Relationship, Index
|
from sqlmodel import Field, Relationship, Index
|
||||||
|
|
||||||
from sqlmodel_ext import SQLModelBase, TableBaseMixin
|
from sqlmodel_ext import SQLModelBase, TableBaseMixin, Str255
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .object import Object
|
from .object import Object
|
||||||
@@ -17,7 +17,7 @@ class SourceLink(SQLModelBase, TableBaseMixin):
|
|||||||
Index("ix_sourcelink_object_name", "object_id", "name"),
|
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"})
|
downloads: int = Field(default=0, sa_column_kwargs={"server_default": "0"})
|
||||||
|
|||||||
@@ -1,23 +1,60 @@
|
|||||||
|
|
||||||
from typing import TYPE_CHECKING
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
from uuid import UUID
|
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:
|
if TYPE_CHECKING:
|
||||||
from .user import User
|
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):
|
class StoragePack(SQLModelBase, TableBaseMixin):
|
||||||
"""容量包模型"""
|
"""容量包模型"""
|
||||||
|
|
||||||
name: str = Field(max_length=255, description="容量包名称")
|
name: Str255
|
||||||
active_time: datetime | None = Field(default=None, description="激活时间")
|
"""容量包名称"""
|
||||||
expired_time: datetime | None = Field(default=None, index=True, description="过期时间")
|
|
||||||
size: int = Field(description="容量包大小(字节)")
|
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(
|
user_id: UUID = Field(
|
||||||
foreign_key="user.id",
|
foreign_key="user.id",
|
||||||
@@ -25,6 +62,17 @@ class StoragePack(SQLModelBase, TableBaseMixin):
|
|||||||
ondelete="CASCADE"
|
ondelete="CASCADE"
|
||||||
)
|
)
|
||||||
"""所属用户UUID"""
|
"""所属用户UUID"""
|
||||||
|
|
||||||
# 关系
|
# 关系
|
||||||
user: "User" = Relationship(back_populates="storage_packs")
|
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,
|
||||||
|
)
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from datetime import datetime
|
|||||||
|
|
||||||
from sqlmodel import Field, Relationship, UniqueConstraint, Column, func, 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:
|
if TYPE_CHECKING:
|
||||||
from .user import User
|
from .user import User
|
||||||
@@ -24,13 +24,13 @@ class Tag(SQLModelBase, TableBaseMixin):
|
|||||||
|
|
||||||
__table_args__ = (UniqueConstraint("name", "user_id", name="uq_tag_name_user"),)
|
__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)
|
type: TagType = Field(default=TagType.MANUAL)
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from uuid import UUID
|
|||||||
|
|
||||||
from sqlmodel import Field
|
from sqlmodel import Field
|
||||||
|
|
||||||
from sqlmodel_ext import SQLModelBase, UUIDTableBaseMixin
|
from sqlmodel_ext import SQLModelBase, UUIDTableBaseMixin, Str100
|
||||||
|
|
||||||
from .color import ChromaticColor, NeutralColor, ThemeColorsBase
|
from .color import ChromaticColor, NeutralColor, ThemeColorsBase
|
||||||
|
|
||||||
@@ -11,7 +11,7 @@ from .color import ChromaticColor, NeutralColor, ThemeColorsBase
|
|||||||
class ThemePresetBase(SQLModelBase):
|
class ThemePresetBase(SQLModelBase):
|
||||||
"""主题预设基础字段"""
|
"""主题预设基础字段"""
|
||||||
|
|
||||||
name: str = Field(max_length=100)
|
name: Str100
|
||||||
"""预设名称"""
|
"""预设名称"""
|
||||||
|
|
||||||
is_default: bool = False
|
is_default: bool = False
|
||||||
@@ -42,7 +42,7 @@ class ThemePresetBase(SQLModelBase):
|
|||||||
class ThemePreset(ThemePresetBase, UUIDTableBaseMixin):
|
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):
|
class ThemePresetCreateRequest(SQLModelBase):
|
||||||
"""创建主题预设请求 DTO"""
|
"""创建主题预设请求 DTO"""
|
||||||
|
|
||||||
name: str = Field(max_length=100)
|
name: Str100
|
||||||
"""预设名称"""
|
"""预设名称"""
|
||||||
|
|
||||||
colors: ThemeColorsBase
|
colors: ThemeColorsBase
|
||||||
@@ -61,7 +61,7 @@ class ThemePresetCreateRequest(SQLModelBase):
|
|||||||
class ThemePresetUpdateRequest(SQLModelBase):
|
class ThemePresetUpdateRequest(SQLModelBase):
|
||||||
"""更新主题预设请求 DTO"""
|
"""更新主题预设请求 DTO"""
|
||||||
|
|
||||||
name: str | None = Field(default=None, max_length=100)
|
name: Str100 | None = None
|
||||||
"""预设名称(可选)"""
|
"""预设名称(可选)"""
|
||||||
|
|
||||||
colors: ThemeColorsBase | None = None
|
colors: ThemeColorsBase | None = None
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from sqlmodel import Field, Relationship
|
|||||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||||
from sqlmodel.main import RelationshipInfo
|
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 .auth_identity import AuthProviderType
|
||||||
from .color import ChromaticColor, NeutralColor, ThemeColorsBase
|
from .color import ChromaticColor, NeutralColor, ThemeColorsBase
|
||||||
@@ -23,6 +23,7 @@ if TYPE_CHECKING:
|
|||||||
from .download import Download
|
from .download import Download
|
||||||
from .object import Object
|
from .object import Object
|
||||||
from .order import Order
|
from .order import Order
|
||||||
|
from .redeem import Redeem
|
||||||
from .share import Share
|
from .share import Share
|
||||||
from .storage_pack import StoragePack
|
from .storage_pack import StoragePack
|
||||||
from .tag import Tag
|
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)
|
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)
|
score: int = Field(default=0, sa_column_kwargs={"server_default": "0"}, ge=0)
|
||||||
@@ -570,6 +571,14 @@ class User(UserBase, UUIDTableBaseMixin):
|
|||||||
back_populates="user",
|
back_populates="user",
|
||||||
sa_relationship_kwargs={"cascade": "all, delete-orphan"}
|
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(
|
shares: list["Share"] = Relationship(
|
||||||
back_populates="user",
|
back_populates="user",
|
||||||
sa_relationship_kwargs={"cascade": "all, delete-orphan"}
|
sa_relationship_kwargs={"cascade": "all, delete-orphan"}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from uuid import UUID
|
|||||||
from sqlalchemy import Column, Text
|
from sqlalchemy import Column, Text
|
||||||
from sqlmodel import Field, Relationship
|
from sqlmodel import Field, Relationship
|
||||||
|
|
||||||
from sqlmodel_ext import SQLModelBase, TableBaseMixin
|
from sqlmodel_ext import SQLModelBase, TableBaseMixin, Str32, Str100, Str255
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .user import User
|
from .user import User
|
||||||
@@ -51,7 +51,7 @@ class AuthnDetailResponse(SQLModelBase):
|
|||||||
class AuthnRenameRequest(SQLModelBase):
|
class AuthnRenameRequest(SQLModelBase):
|
||||||
"""WebAuthn 凭证重命名请求 DTO"""
|
"""WebAuthn 凭证重命名请求 DTO"""
|
||||||
|
|
||||||
name: str = Field(max_length=100)
|
name: Str100
|
||||||
"""新的凭证名称"""
|
"""新的凭证名称"""
|
||||||
|
|
||||||
|
|
||||||
@@ -60,7 +60,7 @@ class AuthnRenameRequest(SQLModelBase):
|
|||||||
class UserAuthn(SQLModelBase, TableBaseMixin):
|
class UserAuthn(SQLModelBase, TableBaseMixin):
|
||||||
"""用户 WebAuthn 凭证模型,与 User 为多对一关系"""
|
"""用户 WebAuthn 凭证模型,与 User 为多对一关系"""
|
||||||
|
|
||||||
credential_id: str = Field(max_length=255, unique=True, index=True)
|
credential_id: Str255 = Field(unique=True, index=True)
|
||||||
"""凭证 ID,Base64URL 编码"""
|
"""凭证 ID,Base64URL 编码"""
|
||||||
|
|
||||||
credential_public_key: str = Field(sa_column=Column(Text))
|
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)
|
sign_count: int = Field(default=0, ge=0)
|
||||||
"""签名计数器,用于防重放攻击"""
|
"""签名计数器,用于防重放攻击"""
|
||||||
|
|
||||||
credential_device_type: str = Field(max_length=32)
|
credential_device_type: Str32
|
||||||
"""凭证设备类型:'single_device' 或 'multi_device'"""
|
"""凭证设备类型:'single_device' 或 'multi_device'"""
|
||||||
|
|
||||||
credential_backed_up: bool = Field(default=False)
|
credential_backed_up: bool = Field(default=False)
|
||||||
"""凭证是否已备份"""
|
"""凭证是否已备份"""
|
||||||
|
|
||||||
transports: str | None = Field(default=None, max_length=255)
|
transports: Str255 | None = None
|
||||||
"""支持的传输方式,逗号分隔,如 'usb,nfc,ble,internal'"""
|
"""支持的传输方式,逗号分隔,如 'usb,nfc,ble,internal'"""
|
||||||
|
|
||||||
name: str | None = Field(default=None, max_length=100)
|
name: Str100 | None = None
|
||||||
"""用户自定义的凭证名称,便于识别"""
|
"""用户自定义的凭证名称,便于识别"""
|
||||||
|
|
||||||
# 外键
|
# 外键
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from uuid import UUID
|
|||||||
|
|
||||||
from sqlmodel import Field, Relationship, UniqueConstraint
|
from sqlmodel import Field, Relationship, UniqueConstraint
|
||||||
|
|
||||||
from sqlmodel_ext import SQLModelBase, TableBaseMixin
|
from sqlmodel_ext import SQLModelBase, TableBaseMixin, Str255
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .user import User
|
from .user import User
|
||||||
@@ -20,7 +20,7 @@ if TYPE_CHECKING:
|
|||||||
class WebDAVBase(SQLModelBase):
|
class WebDAVBase(SQLModelBase):
|
||||||
"""WebDAV 账户基础字段"""
|
"""WebDAV 账户基础字段"""
|
||||||
|
|
||||||
name: str = Field(max_length=255)
|
name: Str255
|
||||||
"""账户名称(同一用户下唯一)"""
|
"""账户名称(同一用户下唯一)"""
|
||||||
|
|
||||||
root: str = Field(default="/", sa_column_kwargs={"server_default": "'/'"})
|
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"),)
|
__table_args__ = (UniqueConstraint("name", "user_id", name="uq_webdav_name_user"),)
|
||||||
|
|
||||||
password: str = Field(max_length=255)
|
password: Str255
|
||||||
"""密码(Argon2 哈希)"""
|
"""密码(Argon2 哈希)"""
|
||||||
|
|
||||||
# 外键
|
# 外键
|
||||||
@@ -60,10 +60,10 @@ class WebDAV(WebDAVBase, TableBaseMixin):
|
|||||||
class WebDAVCreateRequest(SQLModelBase):
|
class WebDAVCreateRequest(SQLModelBase):
|
||||||
"""创建 WebDAV 账户请求"""
|
"""创建 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 = "/"
|
root: str = "/"
|
||||||
@@ -79,7 +79,7 @@ class WebDAVCreateRequest(SQLModelBase):
|
|||||||
class WebDAVUpdateRequest(SQLModelBase):
|
class WebDAVUpdateRequest(SQLModelBase):
|
||||||
"""更新 WebDAV 账户请求"""
|
"""更新 WebDAV 账户请求"""
|
||||||
|
|
||||||
password: str | None = Field(default=None, min_length=1, max_length=255)
|
password: Str255 | None = Field(default=None, min_length=1)
|
||||||
"""新密码(为 None 时不修改)"""
|
"""新密码(为 None 时不修改)"""
|
||||||
|
|
||||||
root: str | None = None
|
root: str | None = None
|
||||||
|
|||||||
8
tests/fixtures/users.py
vendored
8
tests/fixtures/users.py
vendored
@@ -71,7 +71,7 @@ class UserFactory:
|
|||||||
is_verified=True,
|
is_verified=True,
|
||||||
user_id=user.id,
|
user_id=user.id,
|
||||||
)
|
)
|
||||||
await identity.save(session)
|
identity = await identity.save(session)
|
||||||
|
|
||||||
return user
|
return user
|
||||||
|
|
||||||
@@ -123,7 +123,7 @@ class UserFactory:
|
|||||||
is_verified=True,
|
is_verified=True,
|
||||||
user_id=admin.id,
|
user_id=admin.id,
|
||||||
)
|
)
|
||||||
await identity.save(session)
|
identity = await identity.save(session)
|
||||||
|
|
||||||
return admin
|
return admin
|
||||||
|
|
||||||
@@ -170,7 +170,7 @@ class UserFactory:
|
|||||||
is_verified=True,
|
is_verified=True,
|
||||||
user_id=banned_user.id,
|
user_id=banned_user.id,
|
||||||
)
|
)
|
||||||
await identity.save(session)
|
identity = await identity.save(session)
|
||||||
|
|
||||||
return banned_user
|
return banned_user
|
||||||
|
|
||||||
@@ -219,6 +219,6 @@ class UserFactory:
|
|||||||
is_verified=True,
|
is_verified=True,
|
||||||
user_id=user.id,
|
user_id=user.id,
|
||||||
)
|
)
|
||||||
await identity.save(session)
|
identity = await identity.save(session)
|
||||||
|
|
||||||
return user
|
return user
|
||||||
|
|||||||
250
uv.lock
generated
250
uv.lock
generated
@@ -238,11 +238,11 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cachetools"
|
name = "cachetools"
|
||||||
version = "7.0.1"
|
version = "7.0.4"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
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 = [
|
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]]
|
[[package]]
|
||||||
@@ -282,11 +282,11 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "certifi"
|
name = "certifi"
|
||||||
version = "2026.1.4"
|
version = "2026.2.25"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
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 = [
|
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]]
|
[[package]]
|
||||||
@@ -625,7 +625,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fastapi"
|
name = "fastapi"
|
||||||
version = "0.132.0"
|
version = "0.135.1"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "annotated-doc" },
|
{ name = "annotated-doc" },
|
||||||
@@ -634,9 +634,9 @@ dependencies = [
|
|||||||
{ name = "typing-extensions" },
|
{ name = "typing-extensions" },
|
||||||
{ name = "typing-inspection" },
|
{ 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 = [
|
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]
|
[package.optional-dependencies]
|
||||||
@@ -653,16 +653,16 @@ standard = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fastapi-cli"
|
name = "fastapi-cli"
|
||||||
version = "0.0.23"
|
version = "0.0.24"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "rich-toolkit" },
|
{ name = "rich-toolkit" },
|
||||||
{ name = "typer" },
|
{ name = "typer" },
|
||||||
{ name = "uvicorn", extra = ["standard"] },
|
{ 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 = [
|
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]
|
[package.optional-dependencies]
|
||||||
@@ -673,7 +673,7 @@ standard = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fastapi-cloud-cli"
|
name = "fastapi-cloud-cli"
|
||||||
version = "0.13.0"
|
version = "0.14.1"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "fastar" },
|
{ name = "fastar" },
|
||||||
@@ -685,9 +685,9 @@ dependencies = [
|
|||||||
{ name = "typer" },
|
{ name = "typer" },
|
||||||
{ name = "uvicorn", extra = ["standard"] },
|
{ 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 = [
|
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]]
|
[[package]]
|
||||||
@@ -1630,11 +1630,11 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "python-dotenv"
|
name = "python-dotenv"
|
||||||
version = "1.2.1"
|
version = "1.2.2"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
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 = [
|
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]]
|
[[package]]
|
||||||
@@ -1684,11 +1684,11 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "redis"
|
name = "redis"
|
||||||
version = "7.2.0"
|
version = "7.3.0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
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 = [
|
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]
|
[package.optional-dependencies]
|
||||||
@@ -1711,16 +1711,16 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rich-toolkit"
|
name = "rich-toolkit"
|
||||||
version = "0.19.4"
|
version = "0.19.7"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "click" },
|
{ name = "click" },
|
||||||
{ name = "rich" },
|
{ name = "rich" },
|
||||||
{ name = "typing-extensions" },
|
{ 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 = [
|
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]]
|
[[package]]
|
||||||
@@ -1778,15 +1778,15 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sentry-sdk"
|
name = "sentry-sdk"
|
||||||
version = "2.53.0"
|
version = "2.54.0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "certifi" },
|
{ name = "certifi" },
|
||||||
{ name = "urllib3" },
|
{ 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 = [
|
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]]
|
[[package]]
|
||||||
@@ -1809,37 +1809,41 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sqlalchemy"
|
name = "sqlalchemy"
|
||||||
version = "2.0.46"
|
version = "2.0.48"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
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 = "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" },
|
{ 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 = [
|
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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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]]
|
[[package]]
|
||||||
@@ -2126,78 +2130,86 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "yarl"
|
name = "yarl"
|
||||||
version = "1.22.0"
|
version = "1.23.0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "idna" },
|
{ name = "idna" },
|
||||||
{ name = "multidict" },
|
{ name = "multidict" },
|
||||||
{ name = "propcache" },
|
{ 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 = [
|
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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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" },
|
||||||
]
|
]
|
||||||
|
|||||||
Reference in New Issue
Block a user