From 6d6c70cea8010b1d00d06a9ba1cc91127cf8d777 Mon Sep 17 00:00:00 2001 From: Winnie <3308978791@qq.com> Date: Thu, 30 Jul 2026 13:43:29 +0800 Subject: [PATCH] refactor: integrate model platform backend --- .env.example | 33 + Makefile | 42 +- Makefile.legacy | 28 + alembic.ini | 41 + backend/Dockerfile | 20 +- backend/README.md | 5 + backend/pyproject.toml | 29 +- backend/src/backend/__init__.py | 3 +- backend/src/backend/admin.py | 218 ++ backend/src/backend/dependencies.py | 79 + backend/src/backend/file_locks.py | 110 + backend/src/backend/jupyter.py | 84 + backend/src/backend/legacy_jupyter_auth.py | 147 + backend/src/backend/main.py | 227 +- backend/src/backend/resources.py | 293 ++ backend/src/backend/runtime_client.py | 104 + backend/src/backend/schedule_runs.py | 343 ++ backend/src/backend/schedule_schemas.py | 214 ++ backend/src/backend/schedules.py | 1076 ++++++ backend/src/backend/schemas.py | 72 + backend/src/backend/scripts.py | 925 +++++ backend/src/backend/storage_api.py | 681 ++++ backend/src/backend/storage_client.py | 116 + backend/src/backend/storage_schemas.py | 79 + common/README.md | 7 +- common/pyproject.toml | 20 +- common/src/common/__init__.py | 3 +- common/src/common/db/__init__.py | 22 +- common/src/common/db/models.py | 878 +++++ common/src/common/db/session.py | 56 +- common/src/common/eventing.py | 71 + common/src/common/ids.py | 17 + common/src/common/service_app.py | 79 + common/src/common/storage/__init__.py | 8 +- common/src/common/storage/rustfs.py | 131 +- contracts/README.md | 15 + contracts/__init__.py | 1 + contracts/demo-core-v1.md | 57 + contracts/events/README.md | 11 + contracts/events/event-envelope-v1.json | 72 + contracts/events/job-node-execute-v1.json | 125 + contracts/events/job-node-finished-v1.json | 162 + .../events/schedule-run-requested-v1.json | 255 ++ contracts/locks/README.md | 4 + contracts/locks/file-edit-lock-v1.md | 63 + contracts/openapi/README.md | 11 + contracts/openapi/demo-core-extension-v1.yaml | 1093 ++++++ contracts/openapi/platform-api-v1.yaml | 3080 +++++++++++++++++ .../openapi/runtime-api-internal-v1.yaml | 699 ++++ .../openapi/storage-api-internal-v1.yaml | 550 +++ contracts/runtime/README.md | 26 + contracts/runtime/__init__.py | 1 + contracts/runtime/jupyter-proxy-v1.md | 65 + contracts/runtime/runtime_adapter.py | 70 + .../schedules/schedule-definition-api-v1.md | 122 + .../.ipynb_checkpoints/test2-checkpoint.py | 9 + .../.ipynb_checkpoints/test-checkpoint.ipynb | 43 + .../admin-zhang/notebooks/NotebookDemo.ipynb | 67 + .../users/admin-zhang/notebooks/test.ipynb | 59 + .../model-dev/users/admin-zhang/test2.py | 9 + deploy/demo/NotebookDemo.ipynb | 67 + docker-compose.legacy.yml | 60 + docker-compose.yml | 220 +- migrations/README.md | 71 + migrations/__init__.py | 1 + migrations/data/README.md | 69 + migrations/data/__init__.py | 1 + migrations/data/migrate_legacy_workspaces.py | 413 +++ migrations/data/migrate_system_json.py | 504 +++ migrations/env.py | 127 + migrations/script.py.mako | 28 + .../20260724_0001_v1_schema_baseline.py | 652 ++++ .../20260728_0002_demo_workspaces_users.py | 112 + ...60728_0003_schedule_artifact_visibility.py | 41 + migrations/versions/README.md | 7 + nginx/default.conf.template | 76 + runtime/Dockerfile | 30 +- runtime/README.md | 4 + runtime/pyproject.toml | 30 +- runtime/src/runtime/__init__.py | 3 +- runtime/src/runtime/legacy_process_runtime.py | 449 +++ runtime/src/runtime/main.py | 1553 ++++++--- runtime/src/runtime/providers/__init__.py | 1 + .../src/runtime/providers/shared_jupyter.py | 203 ++ runtime/src/runtime/redis_lock.py | 110 + runtime/src/runtime/runtime_lifecycle.py | 308 ++ runtime/src/runtime/schemas.py | 40 + schedule/Dockerfile | 12 + schedule/README.md | 4 + schedule/pyproject.toml | 30 +- schedule/src/schedule/__init__.py | 3 +- schedule/src/schedule/execution.py | 202 ++ schedule/src/schedule/main.py | 56 +- schedule/src/schedule/notebook_runner.py | 83 + schedule/src/schedule/service.py | 903 +++++ schedule/src/schedule/storage_client.py | 41 + uv.lock | 2426 ++++++------- 97 files changed, 19724 insertions(+), 2146 deletions(-) create mode 100644 .env.example create mode 100644 Makefile.legacy create mode 100644 alembic.ini create mode 100644 backend/src/backend/admin.py create mode 100644 backend/src/backend/dependencies.py create mode 100644 backend/src/backend/file_locks.py create mode 100644 backend/src/backend/jupyter.py create mode 100644 backend/src/backend/legacy_jupyter_auth.py create mode 100644 backend/src/backend/resources.py create mode 100644 backend/src/backend/runtime_client.py create mode 100644 backend/src/backend/schedule_runs.py create mode 100644 backend/src/backend/schedule_schemas.py create mode 100644 backend/src/backend/schedules.py create mode 100644 backend/src/backend/schemas.py create mode 100644 backend/src/backend/scripts.py create mode 100644 backend/src/backend/storage_api.py create mode 100644 backend/src/backend/storage_client.py create mode 100644 backend/src/backend/storage_schemas.py create mode 100644 common/src/common/db/models.py create mode 100644 common/src/common/eventing.py create mode 100644 common/src/common/ids.py create mode 100644 common/src/common/service_app.py create mode 100644 contracts/README.md create mode 100644 contracts/__init__.py create mode 100644 contracts/demo-core-v1.md create mode 100644 contracts/events/README.md create mode 100644 contracts/events/event-envelope-v1.json create mode 100644 contracts/events/job-node-execute-v1.json create mode 100644 contracts/events/job-node-finished-v1.json create mode 100644 contracts/events/schedule-run-requested-v1.json create mode 100644 contracts/locks/README.md create mode 100644 contracts/locks/file-edit-lock-v1.md create mode 100644 contracts/openapi/README.md create mode 100644 contracts/openapi/demo-core-extension-v1.yaml create mode 100644 contracts/openapi/platform-api-v1.yaml create mode 100644 contracts/openapi/runtime-api-internal-v1.yaml create mode 100644 contracts/openapi/storage-api-internal-v1.yaml create mode 100644 contracts/runtime/README.md create mode 100644 contracts/runtime/__init__.py create mode 100644 contracts/runtime/jupyter-proxy-v1.md create mode 100644 contracts/runtime/runtime_adapter.py create mode 100644 contracts/schedules/schedule-definition-api-v1.md create mode 100644 deploy/data/workspaces/model-dev/users/admin-zhang/.ipynb_checkpoints/test2-checkpoint.py create mode 100644 deploy/data/workspaces/model-dev/users/admin-zhang/notebooks/.ipynb_checkpoints/test-checkpoint.ipynb create mode 100644 deploy/data/workspaces/model-dev/users/admin-zhang/notebooks/NotebookDemo.ipynb create mode 100644 deploy/data/workspaces/model-dev/users/admin-zhang/notebooks/test.ipynb create mode 100644 deploy/data/workspaces/model-dev/users/admin-zhang/test2.py create mode 100644 deploy/demo/NotebookDemo.ipynb create mode 100644 docker-compose.legacy.yml create mode 100644 migrations/README.md create mode 100644 migrations/__init__.py create mode 100644 migrations/data/README.md create mode 100644 migrations/data/__init__.py create mode 100644 migrations/data/migrate_legacy_workspaces.py create mode 100644 migrations/data/migrate_system_json.py create mode 100644 migrations/env.py create mode 100644 migrations/script.py.mako create mode 100644 migrations/versions/20260724_0001_v1_schema_baseline.py create mode 100644 migrations/versions/20260728_0002_demo_workspaces_users.py create mode 100644 migrations/versions/20260728_0003_schedule_artifact_visibility.py create mode 100644 migrations/versions/README.md create mode 100644 nginx/default.conf.template create mode 100644 runtime/src/runtime/legacy_process_runtime.py create mode 100644 runtime/src/runtime/providers/__init__.py create mode 100644 runtime/src/runtime/providers/shared_jupyter.py create mode 100644 runtime/src/runtime/redis_lock.py create mode 100644 runtime/src/runtime/runtime_lifecycle.py create mode 100644 runtime/src/runtime/schemas.py create mode 100644 schedule/Dockerfile create mode 100644 schedule/src/schedule/execution.py create mode 100644 schedule/src/schedule/notebook_runner.py create mode 100644 schedule/src/schedule/service.py create mode 100644 schedule/src/schedule/storage_client.py diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..aae7d7f --- /dev/null +++ b/.env.example @@ -0,0 +1,33 @@ +COMPOSE_PROJECT_NAME=model-platform-refactored + +# Local development ports +NGINX_PORT=8080 +GATEWAY_PORT=8081 +MYSQL_PORT=3308 +REDIS_PORT=6380 +RUSTFS_API_PORT=9010 +RUSTFS_CONSOLE_PORT=9011 +BACKEND_PORT=8010 +RUNTIME_PORT=8012 +SCHEDULE_PORT=8013 + +# Jupyter runs on the internal Compose network only in step 14. +JUPYTER_IMAGE=quay.io/jupyter/base-notebook:2025-12-31 +JUPYTER_TOKEN=ChangeMe_Jupyter_Internal_2026 + +# MySQL 8 local development credentials +MYSQL_DATABASE=model_platform +MYSQL_USER=model_platform +MYSQL_PASSWORD=ChangeMe_MySQL_App_2026 +MYSQL_ROOT_PASSWORD=ChangeMe_MySQL_Root_2026 + +# Redis local development credential +REDIS_PASSWORD=ChangeMe_Redis_2026 + +# RustFS local development image and credentials +RUSTFS_IMAGE=rustfs/rustfs:latest +RUSTFS_ACCESS_KEY=modelplatform +RUSTFS_SECRET_KEY=ChangeMe_RustFS_2026 + +# Internal service authentication; replace in every non-local environment. +INTERNAL_SERVICE_TOKEN=ChangeMe_Internal_Service_2026 diff --git a/Makefile b/Makefile index 6ff8fbf..c8f71cd 100644 --- a/Makefile +++ b/Makefile @@ -1,28 +1,22 @@ -# ==================== 环境变量配置 ==================== -# 设置默认值(如果命令行没传,就用这里的默认路径) -WORKSPACES_ROOT ?= ./test/workspaces -PUBLIC_BASE_URL ?= http://192.168.139.3 -RUNTIME_HOST ?= 0.0.0.0 -RUNTIME_PORT ?= 8001 -RUNTIME_BASE_URL ?= http://127.0.0.1:8001 -BACKEND_HOST ?= 0.0.0.0 -BACKEND_PORT ?= 8000 +.PHONY: sync backend runtime schedule migrate up down -# 将变量导出给 Makefile 启动的所有子进程 -export WORKSPACES_ROOT -export PUBLIC_BASE_URL -export RUNTIME_BASE_URL - -.PHONY: runtime backend - -runtime: - uv run --package runtime uvicorn runtime.main:app --host $(RUNTIME_HOST) --port $(RUNTIME_PORT) - -runtime-dev: - uv run --package runtime uvicorn runtime.main:app --host $(RUNTIME_HOST) --port $(RUNTIME_PORT) --reload +sync: + uv sync --all-packages backend: - uv run --package backend uvicorn backend.main:app --host $(BACKEND_HOST) --port $(BACKEND_PORT) + uv run --package backend uvicorn backend.main:app --host 0.0.0.0 --port 8010 --reload -backend-dev: - uv run --package backend uvicorn backend.main:app --host $(BACKEND_HOST) --port $(BACKEND_PORT) --reload \ No newline at end of file +runtime: + uv run --package runtime uvicorn runtime.main:app --host 0.0.0.0 --port 8012 --reload + +schedule: + uv run --package schedule uvicorn schedule.main:app --host 0.0.0.0 --port 8013 --reload + +migrate: + uv run --package backend alembic upgrade head + +up: + docker compose up -d --build + +down: + docker compose down diff --git a/Makefile.legacy b/Makefile.legacy new file mode 100644 index 0000000..6ff8fbf --- /dev/null +++ b/Makefile.legacy @@ -0,0 +1,28 @@ +# ==================== 环境变量配置 ==================== +# 设置默认值(如果命令行没传,就用这里的默认路径) +WORKSPACES_ROOT ?= ./test/workspaces +PUBLIC_BASE_URL ?= http://192.168.139.3 +RUNTIME_HOST ?= 0.0.0.0 +RUNTIME_PORT ?= 8001 +RUNTIME_BASE_URL ?= http://127.0.0.1:8001 +BACKEND_HOST ?= 0.0.0.0 +BACKEND_PORT ?= 8000 + +# 将变量导出给 Makefile 启动的所有子进程 +export WORKSPACES_ROOT +export PUBLIC_BASE_URL +export RUNTIME_BASE_URL + +.PHONY: runtime backend + +runtime: + uv run --package runtime uvicorn runtime.main:app --host $(RUNTIME_HOST) --port $(RUNTIME_PORT) + +runtime-dev: + uv run --package runtime uvicorn runtime.main:app --host $(RUNTIME_HOST) --port $(RUNTIME_PORT) --reload + +backend: + uv run --package backend uvicorn backend.main:app --host $(BACKEND_HOST) --port $(BACKEND_PORT) + +backend-dev: + uv run --package backend uvicorn backend.main:app --host $(BACKEND_HOST) --port $(BACKEND_PORT) --reload \ No newline at end of file diff --git a/alembic.ini b/alembic.ini new file mode 100644 index 0000000..c1c4732 --- /dev/null +++ b/alembic.ini @@ -0,0 +1,41 @@ +[alembic] +script_location = %(here)s/migrations +prepend_sys_path = . +path_separator = os + +# 真实连接串必须通过 DATABASE_URL 注入,禁止在仓库内保存数据库密码。 +sqlalchemy.url = driver://user:pass@localhost/dbname + +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/backend/Dockerfile b/backend/Dockerfile index 6e57073..5a3c28e 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -1,22 +1,14 @@ -# backend/Dockerfile -FROM python:3.12-slim +FROM python:3.12-slim-bookworm +ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 WORKDIR /app - -# 安装 uv COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/uv - -# 1. 拷贝根目录配置与 lock 文件 COPY pyproject.toml uv.lock ./ - -# 2. 拷贝 common 模块与 backend 模块 COPY common ./common COPY backend ./backend - -# 3. 安装 backend 及其所有依赖 (包括本地 common) -RUN UV_DEFAULT_INDEX="https://pypi.tuna.tsinghua.edu.cn/simple/" uv sync --frozen --no-dev --no-editable --package backend +COPY alembic.ini ./ +COPY migrations ./migrations +RUN uv sync --frozen --no-dev --no-editable --package backend EXPOSE 8000 - -# 4. 启动服务 (通过 uv run 指定运行 backend 包) -CMD ["uv", "run", "--package", "backend", "uvicorn", "backend.main:app", "--host", "0.0.0.0", "--port", "8000"] \ No newline at end of file +CMD ["uv", "run", "--frozen", "--package", "backend", "uvicorn", "backend.main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/backend/README.md b/backend/README.md index e69de29..eea5476 100644 --- a/backend/README.md +++ b/backend/README.md @@ -0,0 +1,5 @@ +# Backend + +统一 FastAPI 管理服务。包含用户、Workspace、脚本、稳定版本、调度定义、 +立即运行以及 RustFS 对象接口。原 `platform_api` 与 `storage_api` 已在此 +模块合并,外部 REST 契约保持不变。 diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 88165c2..818e80d 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -1,28 +1,17 @@ [project] name = "backend" -version = "0.1.0" -description = "Add your description here" -readme = "README.md" -authors = [ - { name = "tao.chen", email = "93983997+taochen-ct@users.noreply.github.com" } -] +version = "0.2.0" requires-python = ">=3.12" dependencies = [ - "boto3>=1.43.57", - "fastapi>=0.140.0", - "httpx>=0.28.1", - "loguru>=0.7.3", - "pydantic>=2.13.4", - "uvicorn>=0.51.0", + "common", + "fastapi==0.116.1", + "uvicorn[standard]==0.35.0", + "httpx==0.28.1", + "croniter==6.2.4", + "alembic==1.18.5", + "cryptography==49.0.0", ] -[project.scripts] -backend = "backend:main" - -[[tool.uv.index]] -url = "https://pypi.tuna.tsinghua.edu.cn/simple/" -default = true - [tool.uv.sources] common = { workspace = true } @@ -30,3 +19,5 @@ common = { workspace = true } requires = ["hatchling"] build-backend = "hatchling.build" +[tool.hatch.build.targets.wheel] +packages = ["src/backend"] diff --git a/backend/src/backend/__init__.py b/backend/src/backend/__init__.py index bc831a6..d90a9a5 100644 --- a/backend/src/backend/__init__.py +++ b/backend/src/backend/__init__.py @@ -1,2 +1 @@ -def main() -> None: - print("Hello from backend!") +"""Platform API application.""" diff --git a/backend/src/backend/admin.py b/backend/src/backend/admin.py new file mode 100644 index 0000000..d911bd7 --- /dev/null +++ b/backend/src/backend/admin.py @@ -0,0 +1,218 @@ +from __future__ import annotations + +from typing import Any, Literal + +from fastapi import APIRouter, Depends, HTTPException, status +from pydantic import BaseModel, ConfigDict, Field +from sqlalchemy import delete, func, or_, select +from sqlalchemy.ext.asyncio import AsyncSession + +from common.db.models import Roles, Users, WorkspaceMembers +from common.ids import new_ulid +from backend.dependencies import ( + RequestContext, + database_session, + request_context, +) + + +router = APIRouter(prefix="/api/v1/admin", tags=["admin"]) + + +class EmployeeCreate(BaseModel): + model_config = ConfigDict(extra="forbid") + + username: str = Field(min_length=2, max_length=64) + display_name: str = Field(min_length=1, max_length=100) + email: str | None = Field(default=None, max_length=255) + role_code: Literal["admin", "developer"] = "developer" + + +class EmployeeUpdate(BaseModel): + model_config = ConfigDict(extra="forbid") + + display_name: str | None = Field(default=None, min_length=1, max_length=100) + email: str | None = Field(default=None, max_length=255) + role_code: Literal["admin", "developer"] | None = None + status: Literal["active", "disabled", "locked"] | None = None + + +def require_admin(context: RequestContext) -> None: + if not context.is_admin: + raise HTTPException(status.HTTP_403_FORBIDDEN, "仅管理员可以管理员工") + + +def employee_payload(user: Users, role: Roles) -> dict[str, Any]: + return { + "user_id": user.user_id, + "username": user.username, + "display_name": user.display_name, + "email": user.email, + "status": user.status, + "role_code": role.role_code, + "role_name": role.role_name, + "created_at": user.created_at.isoformat(), + } + + +async def member_row( + user_id: str, + context: RequestContext, + session: AsyncSession, +) -> tuple[Users, WorkspaceMembers, Roles]: + row = ( + await session.execute( + select(Users, WorkspaceMembers, Roles) + .join( + WorkspaceMembers, + WorkspaceMembers.user_id == Users.user_id, + ) + .join(Roles, Roles.role_id == WorkspaceMembers.role_id) + .where( + WorkspaceMembers.workspace_id == context.workspace.workspace_id, + Users.user_id == user_id, + ) + ) + ).first() + if row is None: + raise HTTPException(status.HTTP_404_NOT_FOUND, "员工不存在") + return row + + +@router.get("/employees") +async def list_employees( + context: RequestContext = Depends(request_context), + session: AsyncSession = Depends(database_session), +) -> dict[str, Any]: + rows = ( + await session.execute( + select(Users, Roles) + .join( + WorkspaceMembers, + WorkspaceMembers.user_id == Users.user_id, + ) + .join(Roles, Roles.role_id == WorkspaceMembers.role_id) + .where( + WorkspaceMembers.workspace_id == context.workspace.workspace_id, + WorkspaceMembers.member_status == "active", + ) + .order_by(Users.created_at, Users.user_id) + ) + ).all() + return { + "request_id": context.request_id, + "data": [employee_payload(user, role) for user, role in rows], + "meta": {"count": len(rows), "can_manage": context.is_admin}, + } + + +@router.post("/employees", status_code=status.HTTP_201_CREATED) +async def create_employee( + payload: EmployeeCreate, + context: RequestContext = Depends(request_context), + session: AsyncSession = Depends(database_session), +) -> dict[str, Any]: + require_admin(context) + username = payload.username.strip() + display_name = payload.display_name.strip() + duplicate_conditions = [Users.username == username] + if payload.email: + duplicate_conditions.append(Users.email == payload.email.strip()) + duplicate = await session.scalar( + select(Users.user_id).where(or_(*duplicate_conditions)) + ) + if duplicate is not None: + raise HTTPException(status.HTTP_409_CONFLICT, "用户名或邮箱已存在") + role = await session.scalar( + select(Roles).where(Roles.role_code == payload.role_code) + ) + if role is None: + raise HTTPException(status.HTTP_422_UNPROCESSABLE_ENTITY, "角色不存在") + user = Users( + user_id=new_ulid(), + username=username, + display_name=display_name, + email=payload.email.strip() if payload.email else None, + password_hash="demo-login-disabled", + status="active", + ) + session.add(user) + session.add( + WorkspaceMembers( + workspace_id=context.workspace.workspace_id, + user_id=user.user_id, + role_id=role.role_id, + member_status="active", + ) + ) + await session.flush() + await session.refresh(user) + return { + "request_id": context.request_id, + "data": employee_payload(user, role), + "meta": {}, + } + + +@router.patch("/employees/{user_id}") +async def update_employee( + user_id: str, + payload: EmployeeUpdate, + context: RequestContext = Depends(request_context), + session: AsyncSession = Depends(database_session), +) -> dict[str, Any]: + require_admin(context) + user, membership, role = await member_row(user_id, context, session) + if payload.display_name is not None: + user.display_name = payload.display_name.strip() + if payload.email is not None: + user.email = payload.email.strip() or None + if payload.status is not None: + user.status = payload.status + if payload.role_code is not None and payload.role_code != role.role_code: + next_role = await session.scalar( + select(Roles).where(Roles.role_code == payload.role_code) + ) + if next_role is None: + raise HTTPException(status.HTTP_422_UNPROCESSABLE_ENTITY, "角色不存在") + membership.role_id = next_role.role_id + role = next_role + await session.flush() + await session.refresh(user) + return { + "request_id": context.request_id, + "data": employee_payload(user, role), + "meta": {}, + } + + +@router.delete("/employees/{user_id}") +async def delete_employee( + user_id: str, + context: RequestContext = Depends(request_context), + session: AsyncSession = Depends(database_session), +) -> dict[str, Any]: + require_admin(context) + if user_id == context.user.user_id: + raise HTTPException(status.HTTP_409_CONFLICT, "不能删除当前登录员工") + user, _, role = await member_row(user_id, context, session) + if role.role_code == "admin": + raise HTTPException(status.HTTP_409_CONFLICT, "管理员账号不能删除") + await session.execute( + delete(WorkspaceMembers).where( + WorkspaceMembers.workspace_id == context.workspace.workspace_id, + WorkspaceMembers.user_id == user_id, + ) + ) + memberships = await session.scalar( + select(func.count()) + .select_from(WorkspaceMembers) + .where(WorkspaceMembers.user_id == user_id) + ) + if memberships == 0: + user.status = "disabled" + return { + "request_id": context.request_id, + "data": {"user_id": user_id, "deleted": True}, + "meta": {}, + } diff --git a/backend/src/backend/dependencies.py b/backend/src/backend/dependencies.py new file mode 100644 index 0000000..f646a77 --- /dev/null +++ b/backend/src/backend/dependencies.py @@ -0,0 +1,79 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import AsyncIterator + +from fastapi import Header, HTTPException, Request, status +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from common.db.models import ( + Roles, + Users, + WorkspaceMembers, + Workspaces, +) +from common.ids import new_ulid + + +@dataclass(frozen=True) +class RequestContext: + request_id: str + user: Users + workspace: Workspaces + role: Roles + + @property + def is_admin(self) -> bool: + return self.role.role_code == "admin" + + +async def database_session(request: Request) -> AsyncIterator[AsyncSession]: + async with request.app.state.session_factory() as session: + try: + yield session + await session.commit() + except Exception: + await session.rollback() + raise + + +async def request_context( + request: Request, + x_user_id: str = Header(alias="X-User-ID"), + x_workspace_id: str = Header(alias="X-Workspace-ID"), + x_request_id: str | None = Header(default=None, alias="X-Request-ID"), +) -> RequestContext: + async with request.app.state.session_factory() as session: + statement = ( + select(Users, Workspaces, Roles) + .join( + WorkspaceMembers, + WorkspaceMembers.user_id == Users.user_id, + ) + .join( + Workspaces, + Workspaces.workspace_id == WorkspaceMembers.workspace_id, + ) + .join(Roles, Roles.role_id == WorkspaceMembers.role_id) + .where( + Users.user_id == x_user_id, + Users.status == "active", + WorkspaceMembers.workspace_id == x_workspace_id, + WorkspaceMembers.member_status == "active", + Workspaces.status == "active", + ) + ) + row = (await session.execute(statement)).one_or_none() + if row is None: + raise HTTPException( + status.HTTP_403_FORBIDDEN, + "active workspace membership is required", + ) + user, workspace, role = row + return RequestContext( + request_id=x_request_id or new_ulid(), + user=user, + workspace=workspace, + role=role, + ) diff --git a/backend/src/backend/file_locks.py b/backend/src/backend/file_locks.py new file mode 100644 index 0000000..7a82cb3 --- /dev/null +++ b/backend/src/backend/file_locks.py @@ -0,0 +1,110 @@ +from __future__ import annotations + +from typing import Any, Awaitable, Callable + +from fastapi import APIRouter, Depends, Request, status +from fastapi.responses import JSONResponse + +from backend.dependencies import ( + RequestContext, + request_context, +) +from backend.runtime_client import RuntimeClientError +from backend.schemas import FileLockTokenRequest + + +router = APIRouter(tags=["file-locks"]) + + +async def runtime_response( + context: RequestContext, + operation: Callable[[], Awaitable[dict[str, Any]]], + *, + success_status: int = status.HTTP_200_OK, +) -> JSONResponse: + try: + data = await operation() + except RuntimeClientError as exc: + error = exc.detail + if not isinstance(error, dict) or "code" not in error: + error = { + "code": "RUNTIME_REQUEST_FAILED", + "message": str(error), + "retryable": exc.status_code >= 500, + "details": {}, + } + return JSONResponse( + status_code=exc.status_code, + content={"request_id": context.request_id, "error": error}, + ) + return JSONResponse( + status_code=success_status, + content={ + "request_id": context.request_id, + "data": data, + "meta": {}, + }, + ) + + +@router.post( + "/api/v1/files/{storage_object_id}/lock", + status_code=status.HTTP_201_CREATED, +) +async def acquire_file_lock( + storage_object_id: str, + request: Request, + context: RequestContext = Depends(request_context), +) -> JSONResponse: + return await runtime_response( + context, + lambda: request.app.state.runtime_client.acquire_file_lock( + { + "workspace_id": context.workspace.workspace_id, + "storage_object_id": storage_object_id, + "user_id": context.user.user_id, + "request_id": context.request_id, + } + ), + success_status=status.HTTP_201_CREATED, + ) + + +@router.post("/api/v1/file-locks/{edit_session_id}/heartbeat") +async def heartbeat_file_lock( + edit_session_id: str, + payload: FileLockTokenRequest, + request: Request, + context: RequestContext = Depends(request_context), +) -> JSONResponse: + return await runtime_response( + context, + lambda: request.app.state.runtime_client.heartbeat_file_lock( + edit_session_id, + { + "workspace_id": context.workspace.workspace_id, + "user_id": context.user.user_id, + "lock_token": payload.lock_token, + }, + ), + ) + + +@router.delete("/api/v1/file-locks/{edit_session_id}") +async def release_file_lock( + edit_session_id: str, + payload: FileLockTokenRequest, + request: Request, + context: RequestContext = Depends(request_context), +) -> JSONResponse: + return await runtime_response( + context, + lambda: request.app.state.runtime_client.release_file_lock( + edit_session_id, + { + "workspace_id": context.workspace.workspace_id, + "user_id": context.user.user_id, + "lock_token": payload.lock_token, + }, + ), + ) diff --git a/backend/src/backend/jupyter.py b/backend/src/backend/jupyter.py new file mode 100644 index 0000000..bf68028 --- /dev/null +++ b/backend/src/backend/jupyter.py @@ -0,0 +1,84 @@ +from __future__ import annotations + +import os + +from fastapi import APIRouter, Depends, Request, status +from fastapi.responses import JSONResponse + +from backend.dependencies import ( + RequestContext, + request_context, +) +from backend.runtime_client import RuntimeClientError +from backend.schemas import ( + CreateJupyterAccessTicketRequest, +) + + +router = APIRouter(tags=["jupyter"]) + + +def cookie_secure() -> bool: + return os.getenv("COOKIE_SECURE", "false").strip().lower() in { + "1", + "true", + "yes", + "on", + } + + +@router.post( + "/api/v1/jupyter/access-tickets", + status_code=status.HTTP_201_CREATED, +) +async def create_jupyter_access_ticket( + payload: CreateJupyterAccessTicketRequest, + request: Request, + context: RequestContext = Depends(request_context), +) -> JSONResponse: + try: + ticket_data = ( + await request.app.state.runtime_client + .create_jupyter_access_ticket( + payload.edit_session_id, + { + "workspace_id": context.workspace.workspace_id, + "user_id": context.user.user_id, + "lock_token": payload.lock_token, + }, + ) + ) + except RuntimeClientError as exc: + error = exc.detail + if not isinstance(error, dict) or "code" not in error: + error = { + "code": "JUPYTER_TICKET_FAILED", + "message": str(error), + "retryable": exc.status_code >= 500, + "details": {}, + } + return JSONResponse( + status_code=exc.status_code, + content={"request_id": context.request_id, "error": error}, + ) + + raw_ticket = ticket_data.pop("ticket") + max_age = int(ticket_data.pop("expires_in_seconds")) + response = JSONResponse( + status_code=status.HTTP_201_CREATED, + content={ + "request_id": context.request_id, + "data": ticket_data, + "meta": {}, + }, + ) + response.set_cookie( + key="jupyter_access", + value=raw_ticket, + max_age=max_age, + path="/jupyter/", + secure=cookie_secure(), + httponly=True, + samesite="lax", + ) + return response diff --git a/backend/src/backend/legacy_jupyter_auth.py b/backend/src/backend/legacy_jupyter_auth.py new file mode 100644 index 0000000..0a474d5 --- /dev/null +++ b/backend/src/backend/legacy_jupyter_auth.py @@ -0,0 +1,147 @@ +# coding=utf-8 +""" +@Time :2026/7/27 +@Author :tao.chen +""" +import os +import re +import httpx +from fastapi import FastAPI, Request, Response, HTTPException, Depends, status +from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials +from typing import Optional +from loguru import logger + +app = FastAPI(title="Jupyter Auth & Router Backend") + +RUNTIME_BASE_URL = os.getenv("RUNTIME_BASE_URL", "http://127.0.0.1:8001") +security = HTTPBearer(auto_error=False) + + +# ------------------------------------------------------------------ +# 1. Runtime 交互 Client +# ------------------------------------------------------------------ +class RuntimeClient: + """与 Runtime 进程管理器服务交互""" + + @staticmethod + async def get_workspace(workspace_id: str) -> Optional[dict]: + """按需查询单个 workspace 进程""" + async with httpx.AsyncClient(base_url=RUNTIME_BASE_URL) as client: + try: + resp = await client.post( + "/api/v1/jupyter", + json={"action": "get", "workspace_id": workspace_id}, + timeout=3.0, + ) + if resp.status_code == 200: + return resp.json() + return None + except httpx.RequestError: + return None + + @staticmethod + async def start_workspace(workspace_id: str) -> dict: + """进程未运行时主动触发启动""" + async with httpx.AsyncClient(base_url=RUNTIME_BASE_URL) as client: + resp = await client.post( + "/api/v1/jupyter", + json={"action": "start", "workspace_id": workspace_id}, + timeout=10.0, + ) + if resp.status_code == 200: + return resp.json() + raise HTTPException( + status_code=500, detail="Failed to start Jupyter instance" + ) + + +# ------------------------------------------------------------------ +# 2. 数据库与权限模拟 (请根据实际 MySQL ORM 修改) +# ------------------------------------------------------------------ +async def check_notebook_is_locked(workspace_id: str, notebook_path: str) -> bool: + """ + 查数据库:判断特定 Notebook 文件是否被锁定 + :param workspace_id: 工作区 ID + :param notebook_path: 相对路径,如 "test.ipynb" 或 "folder/demo.ipynb" + """ + # 模拟锁定数据库:假定 test_locked.ipynb 被锁定 + locked_notebooks = { + ("test1234", "test_locked.ipynb"): True, + } + return locked_notebooks.get((workspace_id, notebook_path), False) + + +def verify_jwt_token(token: str) -> str: + """校验 JWT 令牌""" + if token == "invalid-token": + raise HTTPException(status_code=401, detail="Invalid Authentication Token") + return "user_001" + + +def extract_notebook_path(uri: str, workspace_id: str) -> Optional[str]: + """ + 从原始请求 URI 中提取请求的 .ipynb 文件相对路径 + 例如: /jupyter/test1234/notebooks/folder/test.ipynb -> folder/test.ipynb + """ + pattern = rf"^/jupyter/{re.escape(workspace_id)}/notebooks/(.+\.ipynb)" + match = re.match(pattern, uri) + if match: + return match.group(1) + return None + + +# ------------------------------------------------------------------ +# 3. 核心 Auth 接口 (针对 Nginx auth_request) +# ------------------------------------------------------------------ +@app.get("/api/v1/auth/jupyter") +async def verify_jupyter_access( + request: Request, + response: Response, + auth: Optional[HTTPAuthorizationCredentials] = Depends(security), +): + # 获取 Nginx 传入的元数据 + workspace_id = request.headers.get("X-Original-Workspace-Id") + original_uri = request.headers.get("X-Original-URI", "") + + cookie_token = request.cookies.get("access_token") + bearer_token = auth.credentials if auth else None + token = bearer_token or cookie_token + + # if not token: + # raise HTTPException(status_code=401, detail="Missing Authentication Token") + + if not workspace_id: + raise HTTPException(status_code=400, detail="Missing Workspace ID") + + # 基础身份认证 + # current_user_id = verify_jwt_token(token) + + # 精准锁校验:只有在访问 .ipynb 文件时才检查 is_locked + notebook_path = extract_notebook_path(original_uri, workspace_id) + if notebook_path: + is_locked = await check_notebook_is_locked(workspace_id, notebook_path) + if is_locked: + raise HTTPException( + status_code=403, + detail=f"Notebook '{notebook_path}' is currently locked", + ) + + # 获取或启动 Jupyter 子进程 + ws_info = await RuntimeClient.get_workspace(workspace_id) + + if not ws_info or ws_info.get("status") != "running": + ws_info = await RuntimeClient.start_workspace(workspace_id) + + target_port = ws_info.get("port") + jupyter_token = ws_info.get("token") + jupyter_base_url = ws_info.get("base_url") + + if not target_port: + raise HTTPException( + status_code=500, detail="Jupyter instance returned no port" + ) + + # 通过 Response Header 返回 Upstream 地址与 Token 给 Nginx + response.headers["x-upstream-addr"] = f"{jupyter_base_url}:{target_port}" + response.headers["x-jupyter-internal-token"] = jupyter_token or "" + return {"status": "ok"} diff --git a/backend/src/backend/main.py b/backend/src/backend/main.py index 0a474d5..5991bac 100644 --- a/backend/src/backend/main.py +++ b/backend/src/backend/main.py @@ -1,147 +1,98 @@ -# coding=utf-8 -""" -@Time :2026/7/27 -@Author :tao.chen -""" +from __future__ import annotations + +import asyncio import os -import re +from contextlib import asynccontextmanager +from pathlib import Path +from typing import Any, AsyncIterator + import httpx -from fastapi import FastAPI, Request, Response, HTTPException, Depends, status -from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials -from typing import Optional -from loguru import logger +from fastapi.routing import APIRoute -app = FastAPI(title="Jupyter Auth & Router Backend") - -RUNTIME_BASE_URL = os.getenv("RUNTIME_BASE_URL", "http://127.0.0.1:8001") -security = HTTPBearer(auto_error=False) +from common.db import create_database_engine, create_session_factory +from common.service_app import create_service_app +from common.storage import RustFSObjectStore +from backend.admin import router as admin_router +from backend.file_locks import router as file_locks_router +from backend.jupyter import router as jupyter_router +from backend.resources import router as resources_router +from backend.runtime_client import RuntimeClient +from backend.schedule_runs import router as schedule_runs_router +from backend.schedules import router as schedules_router +from backend.scripts import router as scripts_router +from backend.storage_api import app as storage_app +from backend.storage_client import StorageClient -# ------------------------------------------------------------------ -# 1. Runtime 交互 Client -# ------------------------------------------------------------------ -class RuntimeClient: - """与 Runtime 进程管理器服务交互""" +@asynccontextmanager +async def lifespan(app: Any) -> AsyncIterator[None]: + engine = create_database_engine(os.environ["DATABASE_URL"]) + app.state.session_factory = create_session_factory(engine) + workspace_root = Path( + os.getenv("WORKSPACE_ROOT", "/workspace/workspaces") + ) + workspace_root.mkdir(parents=True, exist_ok=True) - @staticmethod - async def get_workspace(workspace_id: str) -> Optional[dict]: - """按需查询单个 workspace 进程""" - async with httpx.AsyncClient(base_url=RUNTIME_BASE_URL) as client: - try: - resp = await client.post( - "/api/v1/jupyter", - json={"action": "get", "workspace_id": workspace_id}, - timeout=3.0, - ) - if resp.status_code == 200: - return resp.json() - return None - except httpx.RequestError: - return None - - @staticmethod - async def start_workspace(workspace_id: str) -> dict: - """进程未运行时主动触发启动""" - async with httpx.AsyncClient(base_url=RUNTIME_BASE_URL) as client: - resp = await client.post( - "/api/v1/jupyter", - json={"action": "start", "workspace_id": workspace_id}, - timeout=10.0, - ) - if resp.status_code == 200: - return resp.json() - raise HTTPException( - status_code=500, detail="Failed to start Jupyter instance" - ) + # Storage API is now part of the backend process. Platform routers keep + # their existing client contract, but calls are dispatched in-process. + app.state.object_store = RustFSObjectStore( + internal_endpoint=os.getenv( + "RUSTFS_INTERNAL_ENDPOINT", + "http://rustfs:9000", + ), + public_endpoint=os.getenv( + "RUSTFS_PUBLIC_ENDPOINT", + "http://localhost:9000", + ), + access_key=os.environ["RUSTFS_ACCESS_KEY"], + secret_key=os.environ["RUSTFS_SECRET_KEY"], + ) + app.state.default_bucket = os.getenv( + "RUSTFS_DEFAULT_BUCKET", + "model-platform", + ) + await asyncio.to_thread( + app.state.object_store.ensure_bucket, + app.state.default_bucket, + ) + storage_http_client = httpx.AsyncClient( + transport=httpx.ASGITransport(app=app), + base_url="http://backend.internal", + timeout=httpx.Timeout(30.0), + ) + app.state.storage_client = StorageClient( + storage_http_client, + os.environ["INTERNAL_SERVICE_TOKEN"], + ) + runtime_http_client = httpx.AsyncClient( + base_url=os.getenv("RUNTIME_API_URL", "http://runtime:8000"), + timeout=httpx.Timeout(30.0), + ) + app.state.runtime_client = RuntimeClient( + runtime_http_client, + os.environ["INTERNAL_SERVICE_TOKEN"], + ) + try: + yield + finally: + await runtime_http_client.aclose() + await storage_http_client.aclose() + await engine.dispose() -# ------------------------------------------------------------------ -# 2. 数据库与权限模拟 (请根据实际 MySQL ORM 修改) -# ------------------------------------------------------------------ -async def check_notebook_is_locked(workspace_id: str, notebook_path: str) -> bool: - """ - 查数据库:判断特定 Notebook 文件是否被锁定 - :param workspace_id: 工作区 ID - :param notebook_path: 相对路径,如 "test.ipynb" 或 "folder/demo.ipynb" - """ - # 模拟锁定数据库:假定 test_locked.ipynb 被锁定 - locked_notebooks = { - ("test1234", "test_locked.ipynb"): True, - } - return locked_notebooks.get((workspace_id, notebook_path), False) +app = create_service_app( + os.getenv("SERVICE_NAME", "backend"), + lifespan=lifespan, +) +app.include_router(file_locks_router) +app.include_router(jupyter_router) +app.include_router(resources_router) +app.include_router(schedule_runs_router) +app.include_router(schedules_router) +app.include_router(scripts_router) +app.include_router(admin_router) - -def verify_jwt_token(token: str) -> str: - """校验 JWT 令牌""" - if token == "invalid-token": - raise HTTPException(status_code=401, detail="Invalid Authentication Token") - return "user_001" - - -def extract_notebook_path(uri: str, workspace_id: str) -> Optional[str]: - """ - 从原始请求 URI 中提取请求的 .ipynb 文件相对路径 - 例如: /jupyter/test1234/notebooks/folder/test.ipynb -> folder/test.ipynb - """ - pattern = rf"^/jupyter/{re.escape(workspace_id)}/notebooks/(.+\.ipynb)" - match = re.match(pattern, uri) - if match: - return match.group(1) - return None - - -# ------------------------------------------------------------------ -# 3. 核心 Auth 接口 (针对 Nginx auth_request) -# ------------------------------------------------------------------ -@app.get("/api/v1/auth/jupyter") -async def verify_jupyter_access( - request: Request, - response: Response, - auth: Optional[HTTPAuthorizationCredentials] = Depends(security), -): - # 获取 Nginx 传入的元数据 - workspace_id = request.headers.get("X-Original-Workspace-Id") - original_uri = request.headers.get("X-Original-URI", "") - - cookie_token = request.cookies.get("access_token") - bearer_token = auth.credentials if auth else None - token = bearer_token or cookie_token - - # if not token: - # raise HTTPException(status_code=401, detail="Missing Authentication Token") - - if not workspace_id: - raise HTTPException(status_code=400, detail="Missing Workspace ID") - - # 基础身份认证 - # current_user_id = verify_jwt_token(token) - - # 精准锁校验:只有在访问 .ipynb 文件时才检查 is_locked - notebook_path = extract_notebook_path(original_uri, workspace_id) - if notebook_path: - is_locked = await check_notebook_is_locked(workspace_id, notebook_path) - if is_locked: - raise HTTPException( - status_code=403, - detail=f"Notebook '{notebook_path}' is currently locked", - ) - - # 获取或启动 Jupyter 子进程 - ws_info = await RuntimeClient.get_workspace(workspace_id) - - if not ws_info or ws_info.get("status") != "running": - ws_info = await RuntimeClient.start_workspace(workspace_id) - - target_port = ws_info.get("port") - jupyter_token = ws_info.get("token") - jupyter_base_url = ws_info.get("base_url") - - if not target_port: - raise HTTPException( - status_code=500, detail="Jupyter instance returned no port" - ) - - # 通过 Response Header 返回 Upstream 地址与 Token 给 Nginx - response.headers["x-upstream-addr"] = f"{jupyter_base_url}:{target_port}" - response.headers["x-jupyter-internal-token"] = jupyter_token or "" - return {"status": "ok"} +# Reuse the proven storage endpoints without running another FastAPI service. +for route in storage_app.routes: + if isinstance(route, APIRoute) and route.path.startswith("/internal/"): + app.router.routes.append(route) diff --git a/backend/src/backend/resources.py b/backend/src/backend/resources.py new file mode 100644 index 0000000..14a4d7b --- /dev/null +++ b/backend/src/backend/resources.py @@ -0,0 +1,293 @@ +from __future__ import annotations + +from datetime import UTC, datetime +from typing import Any + +from fastapi import APIRouter, Depends, Header, HTTPException, Query, Request, status +from sqlalchemy import or_, select +from sqlalchemy.ext.asyncio import AsyncSession + +from common.db.models import DataResources, StorageObjects +from common.ids import new_ulid +from backend.dependencies import ( + RequestContext, + database_session, + request_context, +) +from backend.schemas import ( + CompleteResourceUploadRequest, + CreateResourceUploadRequest, + DownloadUrlRequest, +) + +router = APIRouter(prefix="/api/v1/data-resources", tags=["data-resources"]) + + +def resource_payload( + resource: DataResources, + storage_object: StorageObjects, +) -> dict[str, Any]: + return { + "resource_id": resource.resource_id, + "workspace_id": resource.workspace_id, + "storage_object_id": resource.storage_object_id, + "owner_user_id": resource.owner_user_id, + "resource_name": resource.resource_name, + "description": resource.description, + "visibility": resource.visibility, + "status": resource.status, + "created_at": resource.created_at.isoformat(), + "updated_at": resource.updated_at.isoformat(), + "file": { + "file_name": storage_object.file_name, + "file_extension": storage_object.file_extension, + "mime_type": storage_object.mime_type, + "size_bytes": storage_object.size_bytes, + "content_hash": storage_object.content_hash, + "object_status": storage_object.object_status, + }, + } + + +def can_view(resource: DataResources, context: RequestContext) -> bool: + return ( + resource.owner_user_id == context.user.user_id + or resource.visibility in {"workspace", "public"} + or context.is_admin + ) + + +@router.post("/uploads", status_code=status.HTTP_201_CREATED) +async def create_resource_upload( + payload: CreateResourceUploadRequest, + request: Request, + context: RequestContext = Depends(request_context), + idempotency_key: str = Header( + min_length=8, + max_length=128, + alias="Idempotency-Key", + ), +) -> dict[str, Any]: + data = await request.app.state.storage_client.create_upload( + { + "workspace_id": context.workspace.workspace_id, + "user_id": context.user.user_id, + "usage_type": "data_resource", + "file_name": payload.file_name, + "content_type": payload.content_type, + "expected_size_bytes": payload.expected_size_bytes, + "expected_hash": payload.expected_hash, + "idempotency_key": idempotency_key, + "url_scope": "public", + } + ) + return {"request_id": context.request_id, "data": data, "meta": {}} + + +@router.post("/uploads/{upload_id}/complete") +async def complete_resource_upload( + upload_id: str, + payload: CompleteResourceUploadRequest, + request: Request, + context: RequestContext = Depends(request_context), + session: AsyncSession = Depends(database_session), +) -> dict[str, Any]: + storage_data = await request.app.state.storage_client.complete_upload( + upload_id, + { + "usage_type": "data_resource", + "visibility": payload.visibility, + "is_immutable": False, + }, + ) + if storage_data["workspace_id"] != context.workspace.workspace_id: + raise HTTPException( + status.HTTP_403_FORBIDDEN, + "upload belongs to another workspace", + ) + if storage_data["owner_user_id"] != context.user.user_id: + raise HTTPException( + status.HTTP_403_FORBIDDEN, + "upload belongs to another user", + ) + existing = await session.scalar( + select(DataResources).where( + DataResources.storage_object_id + == storage_data["storage_object_id"] + ) + ) + reused = existing is not None + if existing is None: + existing = DataResources( + resource_id=new_ulid(), + workspace_id=context.workspace.workspace_id, + storage_object_id=storage_data["storage_object_id"], + owner_user_id=context.user.user_id, + resource_name=payload.resource_name, + description=payload.description, + visibility=payload.visibility, + status="active", + ) + session.add(existing) + await session.flush() + await session.refresh(existing) + storage_object = await session.get( + StorageObjects, + existing.storage_object_id, + ) + if storage_object is None: + raise HTTPException( + status.HTTP_409_CONFLICT, + "storage object metadata is missing", + ) + return { + "request_id": context.request_id, + "data": resource_payload(existing, storage_object), + "meta": {"reused": reused}, + } + + +@router.get("") +async def list_resources( + context: RequestContext = Depends(request_context), + session: AsyncSession = Depends(database_session), + visibility: str | None = Query(default=None), + keyword: str | None = Query(default=None, max_length=100), +) -> dict[str, Any]: + statement = ( + select(DataResources, StorageObjects) + .join( + StorageObjects, + StorageObjects.storage_object_id + == DataResources.storage_object_id, + ) + .where( + DataResources.workspace_id == context.workspace.workspace_id, + DataResources.status == "active", + StorageObjects.object_status == "available", + ) + .order_by(DataResources.updated_at.desc()) + ) + if not context.is_admin: + statement = statement.where( + or_( + DataResources.owner_user_id == context.user.user_id, + DataResources.visibility.in_(["workspace", "public"]), + ) + ) + if visibility: + if visibility not in {"private", "workspace", "public"}: + raise HTTPException( + status.HTTP_422_UNPROCESSABLE_ENTITY, + "invalid visibility", + ) + statement = statement.where(DataResources.visibility == visibility) + if keyword: + statement = statement.where( + DataResources.resource_name.like(f"%{keyword.strip()}%") + ) + rows = (await session.execute(statement)).all() + return { + "request_id": context.request_id, + "data": [ + resource_payload(resource, storage_object) + for resource, storage_object in rows + ], + "meta": {"count": len(rows)}, + } + + +async def get_visible_resource( + resource_id: str, + context: RequestContext, + session: AsyncSession, +) -> tuple[DataResources, StorageObjects]: + row = ( + await session.execute( + select(DataResources, StorageObjects) + .join( + StorageObjects, + StorageObjects.storage_object_id + == DataResources.storage_object_id, + ) + .where( + DataResources.resource_id == resource_id, + DataResources.workspace_id + == context.workspace.workspace_id, + DataResources.status == "active", + ) + ) + ).one_or_none() + if row is None or not can_view(row[0], context): + raise HTTPException(status.HTTP_404_NOT_FOUND, "resource not found") + return row + + +@router.get("/{resource_id}") +async def get_resource( + resource_id: str, + context: RequestContext = Depends(request_context), + session: AsyncSession = Depends(database_session), +) -> dict[str, Any]: + resource, storage_object = await get_visible_resource( + resource_id, + context, + session, + ) + return { + "request_id": context.request_id, + "data": resource_payload(resource, storage_object), + "meta": {}, + } + + +@router.post("/{resource_id}/download-url") +async def resource_download_url( + resource_id: str, + payload: DownloadUrlRequest, + request: Request, + context: RequestContext = Depends(request_context), + session: AsyncSession = Depends(database_session), +) -> dict[str, Any]: + resource, _ = await get_visible_resource( + resource_id, + context, + session, + ) + data = await request.app.state.storage_client.create_download_url( + resource.storage_object_id, + payload.expires_seconds, + ) + return {"request_id": context.request_id, "data": data, "meta": {}} + + +@router.delete("/{resource_id}") +async def delete_resource( + resource_id: str, + request: Request, + context: RequestContext = Depends(request_context), + session: AsyncSession = Depends(database_session), +) -> dict[str, Any]: + resource, _ = await get_visible_resource( + resource_id, + context, + session, + ) + if ( + resource.owner_user_id != context.user.user_id + and not context.is_admin + ): + raise HTTPException( + status.HTTP_403_FORBIDDEN, + "resource can only be deleted by its owner or an administrator", + ) + await request.app.state.storage_client.delete_object( + resource.storage_object_id + ) + resource.status = "deleted" + resource.deleted_at = datetime.now(UTC).replace(tzinfo=None) + return { + "request_id": context.request_id, + "data": {"resource_id": resource_id, "status": "deleted"}, + "meta": {}, + } diff --git a/backend/src/backend/runtime_client.py b/backend/src/backend/runtime_client.py new file mode 100644 index 0000000..e843b30 --- /dev/null +++ b/backend/src/backend/runtime_client.py @@ -0,0 +1,104 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + +import httpx + + +@dataclass(frozen=True) +class RuntimeClientError(Exception): + status_code: int + detail: Any + + +class RuntimeClient: + def __init__( + self, + client: httpx.AsyncClient, + service_token: str, + ) -> None: + self.client = client + self.headers = {"X-Service-Token": service_token} + + async def _request( + self, + method: str, + path: str, + payload: dict[str, Any], + ) -> dict[str, Any]: + try: + response = await self.client.request( + method, + path, + json=payload, + headers=self.headers, + ) + except httpx.RequestError as exc: + raise RuntimeClientError( + 503, + { + "code": "RUNTIME_UNAVAILABLE", + "message": "Runtime Manager 暂时不可用", + "retryable": True, + "details": {}, + }, + ) from exc + if response.is_error: + try: + detail = response.json().get("detail", response.text) + except ValueError: + detail = response.text + raise RuntimeClientError(response.status_code, detail) + return response.json() + + async def acquire_file_lock( + self, + payload: dict[str, Any], + ) -> dict[str, Any]: + return ( + await self._request( + "POST", + "/internal/v1/file-locks/acquire", + payload, + ) + )["data"] + + async def heartbeat_file_lock( + self, + edit_session_id: str, + payload: dict[str, Any], + ) -> dict[str, Any]: + return ( + await self._request( + "POST", + f"/internal/v1/file-locks/{edit_session_id}/heartbeat", + payload, + ) + )["data"] + + async def release_file_lock( + self, + edit_session_id: str, + payload: dict[str, Any], + ) -> dict[str, Any]: + return ( + await self._request( + "DELETE", + f"/internal/v1/file-locks/{edit_session_id}", + payload, + ) + )["data"] + + async def create_jupyter_access_ticket( + self, + edit_session_id: str, + payload: dict[str, Any], + ) -> dict[str, Any]: + return ( + await self._request( + "POST", + f"/internal/v1/jupyter/access-tickets/{edit_session_id}", + payload, + ) + )["data"] diff --git a/backend/src/backend/schedule_runs.py b/backend/src/backend/schedule_runs.py new file mode 100644 index 0000000..3ac862b --- /dev/null +++ b/backend/src/backend/schedule_runs.py @@ -0,0 +1,343 @@ +from __future__ import annotations + +import hashlib +from datetime import UTC, datetime +from typing import Any, Literal + +from fastapi import APIRouter, Depends, Header, HTTPException, Query, status +from pydantic import Field +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from common.db.models import ( + ScheduleNodeRuns, + ScheduleRuns, +) +from common.eventing import add_outbox_event, utcnow +from common.ids import new_ulid +from backend.dependencies import ( + RequestContext, + database_session, + request_context, +) +from backend.schedule_schemas import StrictModel +from backend.schedules import ( + graph_rows, + schedule_row, + validate_dag, +) + + +router = APIRouter(tags=["schedule-runs"]) +RunStatus = Literal[ + "queued", + "running", + "succeeded", + "failed", + "cancelled", + "timed_out", +] + + +class RunScheduleRequest(StrictModel): + reason: str = Field(default="manual_run", min_length=1, max_length=255) + + +def _iso(value: datetime | None) -> str | None: + if value is None: + return None + if value.tzinfo is None: + value = value.replace(tzinfo=UTC) + return value.astimezone(UTC).isoformat() + + +def _normalized_idempotency_key( + workspace_id: str, + schedule_id: str, + value: str, +) -> str: + normalized = value.strip() + if len(normalized) < 8: + raise HTTPException( + status.HTTP_422_UNPROCESSABLE_ENTITY, + "Idempotency-Key must contain at least 8 characters", + ) + digest = hashlib.sha256( + f"{workspace_id}:{schedule_id}:{normalized}".encode("utf-8") + ).hexdigest() + return f"run:v1:{digest}" + + +def _arguments(value: dict[str, Any] | None) -> list[str]: + payload = value or {} + raw = payload.get("_args") + result = [str(item) for item in raw] if isinstance(raw, list) else [] + for key, item in payload.items(): + if key == "_args": + continue + option = f"--{key.replace('_', '-')}" + if item is True: + result.append(option) + elif item is False or item is None: + continue + elif isinstance(item, list): + for list_item in item: + result.extend((option, str(list_item))) + elif isinstance(item, (str, int, float)): + result.extend((option, str(item))) + else: + raise HTTPException( + status.HTTP_422_UNPROCESSABLE_ENTITY, + f"node argument {key!r} must be a scalar or list", + ) + return result + + +def run_summary(item: ScheduleRuns) -> dict[str, Any]: + return { + "run_id": item.run_id, + "schedule_id": item.schedule_id, + "workspace_id": item.workspace_id, + "workflow_version": item.workflow_version, + "trigger_type": item.trigger_type, + "run_status": item.run_status, + "state_version": item.state_version, + "queued_at": _iso(item.queued_at), + "started_at": _iso(item.started_at), + "finished_at": _iso(item.finished_at), + "duration_ms": item.duration_ms, + "error_code": item.error_code, + "error_message": item.error_message, + "logs_object_id": item.logs_object_id, + "result_object_id": item.result_object_id, + } + + +def node_run_payload(item: ScheduleNodeRuns) -> dict[str, Any]: + return { + "node_run_id": item.node_run_id, + "run_id": item.run_id, + "node_id": item.node_id, + "versions_id": item.versions_id, + "attempt_no": item.attempt_no, + "node_status": item.node_status, + "state_version": item.state_version, + "started_at": _iso(item.started_at), + "finished_at": _iso(item.finished_at), + "duration_ms": item.duration_ms, + "exit_code": item.exit_code, + "message": item.message, + "logs_object_id": item.logs_object_id, + "result_object_id": item.result_object_id, + } + + +async def run_detail( + item: ScheduleRuns, + session: AsyncSession, +) -> dict[str, Any]: + node_runs = list( + ( + await session.scalars( + select(ScheduleNodeRuns) + .where(ScheduleNodeRuns.run_id == item.run_id) + .order_by( + ScheduleNodeRuns.created_at, + ScheduleNodeRuns.attempt_no, + ) + ) + ).all() + ) + return { + **run_summary(item), + "node_runs": [node_run_payload(node_run) for node_run in node_runs], + } + + +async def _visible_run( + run_id: str, + context: RequestContext, + session: AsyncSession, +) -> ScheduleRuns: + item = await session.scalar( + select(ScheduleRuns).where( + ScheduleRuns.run_id == run_id, + ScheduleRuns.workspace_id == context.workspace.workspace_id, + ) + ) + if item is None: + raise HTTPException(status.HTTP_404_NOT_FOUND, "schedule run not found") + return item + + +@router.post( + "/api/v1/schedules/{schedule_id}/run", + status_code=status.HTTP_202_ACCEPTED, +) +async def run_schedule_now( + schedule_id: str, + payload: RunScheduleRequest | None = None, + idempotency_key: str = Header(alias="Idempotency-Key"), + context: RequestContext = Depends(request_context), + session: AsyncSession = Depends(database_session), +) -> dict[str, Any]: + del payload + key = _normalized_idempotency_key( + context.workspace.workspace_id, + schedule_id, + idempotency_key, + ) + existing = await session.scalar( + select(ScheduleRuns).where(ScheduleRuns.idempotency_key == key) + ) + if existing is not None: + if ( + existing.workspace_id != context.workspace.workspace_id + or existing.schedule_id != schedule_id + ): + raise HTTPException( + status.HTTP_409_CONFLICT, + "Idempotency-Key belongs to another schedule run", + ) + return { + "request_id": context.request_id, + "data": await run_detail(existing, session), + "meta": {"reused": True}, + } + + schedule = await schedule_row( + schedule_id, + context, + session, + for_update=True, + ) + node_rows, edges = await graph_rows(schedule_id, session) + nodes = [row[0] for row in node_rows] + validation = validate_dag(nodes, edges) + if not validation["valid"] or not nodes: + raise HTTPException( + status.HTTP_409_CONFLICT, + detail={ + "code": "SCHEDULE_DAG_INVALID", + "message": "schedule must contain a valid non-empty DAG", + "errors": validation["errors"], + }, + ) + if len(nodes) > 100 or len(edges) > 500: + raise HTTPException( + status.HTTP_409_CONFLICT, + "schedule exceeds the v1 execution size limit", + ) + + snapshot = { + "schedule_name": schedule.schedule_name, + "workflow_version": schedule.workflow_version, + "max_concurrency": schedule.max_concurrency, + "failure_policy": schedule.failure_policy, + "nodes": [ + { + "node_id": node.node_id, + "node_key": node.node_key, + "versions_id": version.versions_id, + "script_type": script.script_type, + "artifact_object_id": version.artifact_object_id, + "artifact_path": version.artifact_path, + "timeout_seconds": node.timeout_seconds, + "retry_count": node.retry_count, + "retry_interval_sec": node.retry_interval_sec, + "arguments": _arguments(node.arguments_json), + } + for node, version, script in node_rows + ], + "edges": [ + { + "source_node_id": edge.source_node_id, + "target_node_id": edge.target_node_id, + } + for edge in edges + ], + } + now = utcnow() + run = ScheduleRuns( + run_id=new_ulid(), + schedule_id=schedule.schedule_id, + workspace_id=schedule.workspace_id, + workflow_version=schedule.workflow_version, + trigger_type="manual", + idempotency_key=key, + run_status="queued", + state_version=0, + schedule_snapshot=snapshot, + queued_at=now, + triggered_by=context.user.user_id, + ) + session.add(run) + schedule.last_run_at = now + await add_outbox_event( + session, + event_type="schedule.run.requested", + producer="platform-api", + trace_id=context.request_id, + aggregate_type="schedule_run", + aggregate_id=run.run_id, + idempotency_key=key, + payload={ + "workspace_id": run.workspace_id, + "schedule_id": run.schedule_id, + "run_id": run.run_id, + "workflow_version": run.workflow_version, + "trigger_type": run.trigger_type, + "triggered_by": run.triggered_by, + "schedule_snapshot": snapshot, + }, + ) + await session.flush() + await session.refresh(run) + return { + "request_id": context.request_id, + "data": await run_detail(run, session), + "meta": {"reused": False}, + } + + +@router.get("/api/v1/schedule-runs") +async def list_schedule_runs( + schedule_id: str | None = Query(default=None), + run_status: RunStatus | None = Query(default=None, alias="status"), + limit: int = Query(default=50, ge=1, le=200), + context: RequestContext = Depends(request_context), + session: AsyncSession = Depends(database_session), +) -> dict[str, Any]: + statement = select(ScheduleRuns).where( + ScheduleRuns.workspace_id == context.workspace.workspace_id, + ) + if schedule_id: + statement = statement.where(ScheduleRuns.schedule_id == schedule_id) + if run_status: + statement = statement.where(ScheduleRuns.run_status == run_status) + items = list( + ( + await session.scalars( + statement.order_by(ScheduleRuns.queued_at.desc()).limit(limit) + ) + ).all() + ) + return { + "request_id": context.request_id, + "data": [run_summary(item) for item in items], + "meta": {"count": len(items)}, + } + + +@router.get("/api/v1/schedule-runs/{run_id}") +async def get_schedule_run( + run_id: str, + context: RequestContext = Depends(request_context), + session: AsyncSession = Depends(database_session), +) -> dict[str, Any]: + item = await _visible_run(run_id, context, session) + return { + "request_id": context.request_id, + "data": await run_detail(item, session), + "meta": {}, + } diff --git a/backend/src/backend/schedule_schemas.py b/backend/src/backend/schedule_schemas.py new file mode 100644 index 0000000..8d658bb --- /dev/null +++ b/backend/src/backend/schedule_schemas.py @@ -0,0 +1,214 @@ +from __future__ import annotations + +from datetime import datetime +from typing import Any, Literal + +from pydantic import Field, field_validator, model_validator + +from backend.schemas import StrictModel + + +TriggerType = Literal["manual", "cron", "api"] +FailurePolicy = Literal["stop", "continue"] + + +def _required_text(value: str) -> str: + normalized = value.strip() + if not normalized: + raise ValueError("value must not be blank") + return normalized + + +class CreateScheduleRequest(StrictModel): + schedule_name: str = Field(min_length=1, max_length=255) + description: str | None = Field(default=None, max_length=1000) + trigger_type: TriggerType = "cron" + cron_expression: str | None = Field(default=None, max_length=128) + timezone: str = Field(default="Asia/Shanghai", min_length=1, max_length=64) + enabled: bool = False + max_concurrency: int = Field(default=1, ge=1, le=64) + failure_policy: FailurePolicy = "stop" + + @field_validator("schedule_name", "timezone") + @classmethod + def validate_required_text(cls, value: str) -> str: + return _required_text(value) + + @field_validator("description", "cron_expression") + @classmethod + def normalize_optional_text(cls, value: str | None) -> str | None: + if value is None: + return None + normalized = value.strip() + return normalized or None + + @model_validator(mode="after") + def validate_trigger(self) -> "CreateScheduleRequest": + if self.trigger_type == "cron" and not self.cron_expression: + raise ValueError("cron_expression is required for cron schedules") + if self.trigger_type != "cron" and self.cron_expression: + raise ValueError( + "cron_expression is only allowed for cron schedules" + ) + return self + + +class UpdateScheduleRequest(StrictModel): + workflow_version: int = Field(ge=1) + schedule_name: str | None = Field(default=None, min_length=1, max_length=255) + description: str | None = Field(default=None, max_length=1000) + trigger_type: TriggerType | None = None + cron_expression: str | None = Field(default=None, max_length=128) + timezone: str | None = Field(default=None, min_length=1, max_length=64) + enabled: bool | None = None + max_concurrency: int | None = Field(default=None, ge=1, le=64) + failure_policy: FailurePolicy | None = None + + @field_validator("schedule_name", "timezone") + @classmethod + def validate_optional_required_text( + cls, + value: str | None, + ) -> str | None: + return _required_text(value) if value is not None else None + + @field_validator("description", "cron_expression") + @classmethod + def normalize_optional_text(cls, value: str | None) -> str | None: + if value is None: + return None + normalized = value.strip() + return normalized or None + + @model_validator(mode="after") + def require_change(self) -> "UpdateScheduleRequest": + if self.model_fields_set == {"workflow_version"}: + raise ValueError("at least one schedule field must be updated") + for field in { + "schedule_name", + "trigger_type", + "timezone", + "enabled", + "max_concurrency", + "failure_policy", + }: + if field in self.model_fields_set and getattr(self, field) is None: + raise ValueError(f"{field} cannot be null") + return self + + +class WorkflowVersionRequest(StrictModel): + workflow_version: int = Field(ge=1) + + +class CreateScheduleNodeRequest(StrictModel): + workflow_version: int = Field(ge=1) + node_key: str = Field( + min_length=1, + max_length=64, + pattern=r"^[A-Za-z][A-Za-z0-9_-]*$", + ) + node_name: str = Field(min_length=1, max_length=255) + versions_id: str = Field(min_length=26, max_length=26) + timeout_seconds: int = Field(default=600, ge=1, le=86_400) + retry_count: int = Field(default=0, ge=0, le=10) + retry_interval_sec: int = Field(default=5, ge=0, le=3600) + position_x: float = Field(default=0, ge=-100_000, le=100_000) + position_y: float = Field(default=0, ge=-100_000, le=100_000) + arguments_json: dict[str, Any] = Field(default_factory=dict, max_length=100) + env_refs_json: dict[str, str] = Field(default_factory=dict, max_length=100) + + @field_validator("node_key", "node_name") + @classmethod + def validate_required_text(cls, value: str) -> str: + return _required_text(value) + + +class UpdateScheduleNodeRequest(StrictModel): + workflow_version: int = Field(ge=1) + node_name: str | None = Field(default=None, min_length=1, max_length=255) + versions_id: str | None = Field(default=None, min_length=26, max_length=26) + timeout_seconds: int | None = Field(default=None, ge=1, le=86_400) + retry_count: int | None = Field(default=None, ge=0, le=10) + retry_interval_sec: int | None = Field(default=None, ge=0, le=3600) + position_x: float | None = Field( + default=None, + ge=-100_000, + le=100_000, + ) + position_y: float | None = Field( + default=None, + ge=-100_000, + le=100_000, + ) + arguments_json: dict[str, Any] | None = Field( + default=None, + max_length=100, + ) + env_refs_json: dict[str, str] | None = Field( + default=None, + max_length=100, + ) + + @field_validator("node_name") + @classmethod + def validate_optional_required_text( + cls, + value: str | None, + ) -> str | None: + return _required_text(value) if value is not None else None + + @model_validator(mode="after") + def require_change(self) -> "UpdateScheduleNodeRequest": + if self.model_fields_set == {"workflow_version"}: + raise ValueError("at least one node field must be updated") + for field in self.model_fields_set - {"workflow_version"}: + if getattr(self, field) is None: + raise ValueError(f"{field} cannot be null") + return self + + +class CreateScheduleEdgeRequest(StrictModel): + workflow_version: int = Field(ge=1) + source_node_id: str = Field(min_length=26, max_length=26) + target_node_id: str = Field(min_length=26, max_length=26) + condition_expr: str | None = Field(default=None, max_length=1000) + + @field_validator("condition_expr") + @classmethod + def normalize_condition(cls, value: str | None) -> str | None: + if value is None: + return None + normalized = value.strip() + return normalized or None + + @model_validator(mode="after") + def reject_self_edge(self) -> "CreateScheduleEdgeRequest": + if self.source_node_id == self.target_node_id: + raise ValueError("an edge cannot connect a node to itself") + return self + + +class UpdateScheduleEdgeRequest(StrictModel): + workflow_version: int = Field(ge=1) + condition_expr: str | None = Field(default=None, max_length=1000) + + @field_validator("condition_expr") + @classmethod + def normalize_condition(cls, value: str | None) -> str | None: + if value is None: + return None + normalized = value.strip() + return normalized or None + + +class CronPreviewRequest(StrictModel): + cron_expression: str = Field(min_length=1, max_length=128) + timezone: str = Field(default="Asia/Shanghai", min_length=1, max_length=64) + count: int = Field(default=5, ge=1, le=20) + base_time: datetime | None = None + + @field_validator("cron_expression", "timezone") + @classmethod + def validate_required_text(cls, value: str) -> str: + return _required_text(value) diff --git a/backend/src/backend/schedules.py b/backend/src/backend/schedules.py new file mode 100644 index 0000000..59f8925 --- /dev/null +++ b/backend/src/backend/schedules.py @@ -0,0 +1,1076 @@ +from __future__ import annotations + +import heapq +from datetime import UTC, datetime +from decimal import Decimal +from typing import Any +from zoneinfo import ZoneInfo, ZoneInfoNotFoundError + +from croniter import CroniterBadCronError, croniter +from fastapi import APIRouter, Depends, HTTPException, Query, status +from sqlalchemy import delete, func, or_, select +from sqlalchemy.ext.asyncio import AsyncSession + +from common.db.models import ( + ScheduleEdges, + ScheduleNodeRuns, + ScheduleNodes, + Schedules, + Scripts, + Versions, +) +from common.ids import new_ulid +from backend.dependencies import ( + RequestContext, + database_session, + request_context, +) +from backend.schedule_schemas import ( + CreateScheduleEdgeRequest, + CreateScheduleNodeRequest, + CreateScheduleRequest, + CronPreviewRequest, + UpdateScheduleEdgeRequest, + UpdateScheduleNodeRequest, + UpdateScheduleRequest, + WorkflowVersionRequest, +) + + +router = APIRouter(tags=["schedules"]) + + +def _iso(value: datetime | None) -> str | None: + if value is None: + return None + if value.tzinfo is None: + value = value.replace(tzinfo=UTC) + return value.astimezone(UTC).isoformat() + + +def _mysql_utc(value: datetime) -> datetime: + if value.tzinfo is None: + value = value.replace(tzinfo=UTC) + return value.astimezone(UTC).replace(tzinfo=None) + + +def _timezone(value: str) -> ZoneInfo: + try: + return ZoneInfo(value) + except ZoneInfoNotFoundError as exc: + raise HTTPException( + status.HTTP_422_UNPROCESSABLE_ENTITY, + f"unknown timezone: {value}", + ) from exc + + +def cron_preview( + expression: str, + timezone_name: str, + *, + count: int, + base_time: datetime | None = None, +) -> dict[str, Any]: + if len(expression.split()) != 5: + raise HTTPException( + status.HTTP_422_UNPROCESSABLE_ENTITY, + "cron_expression must contain exactly five fields: " + "minute hour day month weekday", + ) + timezone = _timezone(timezone_name) + if base_time is None: + local_base = datetime.now(timezone) + elif base_time.tzinfo is None: + local_base = base_time.replace(tzinfo=timezone) + else: + local_base = base_time.astimezone(timezone) + try: + if not croniter.is_valid(expression, strict=True): + raise CroniterBadCronError(expression) + iterator = croniter(expression, local_base) + occurrences = [ + iterator.get_next(datetime) + for _ in range(count) + ] + except (CroniterBadCronError, ValueError, OverflowError) as exc: + raise HTTPException( + status.HTTP_422_UNPROCESSABLE_ENTITY, + "invalid cron_expression", + ) from exc + return { + "cron_expression": expression, + "timezone": timezone_name, + "base_time": local_base.isoformat(), + "occurrences": [ + { + "local_time": occurrence.isoformat(), + "utc_time": occurrence.astimezone(UTC).isoformat(), + } + for occurrence in occurrences + ], + } + + +def schedule_payload( + item: Schedules, + *, + node_count: int = 0, + edge_count: int = 0, +) -> dict[str, Any]: + return { + "schedule_id": item.schedule_id, + "workspace_id": item.workspace_id, + "schedule_name": item.schedule_name, + "description": item.description, + "trigger_type": item.trigger_type, + "cron_expression": item.cron_expression, + "timezone": item.timezone, + "enabled": bool(item.enabled), + "workflow_version": item.workflow_version, + "max_concurrency": item.max_concurrency, + "failure_policy": item.failure_policy, + "last_run_at": _iso(item.last_run_at), + "next_run_at": _iso(item.next_run_at), + "created_by": item.created_by, + "updated_by": item.updated_by, + "created_at": _iso(item.created_at), + "updated_at": _iso(item.updated_at), + "node_count": node_count, + "edge_count": edge_count, + } + + +def node_payload( + item: ScheduleNodes, + version: Versions, + script: Scripts, +) -> dict[str, Any]: + return { + "node_id": item.node_id, + "schedule_id": item.schedule_id, + "node_key": item.node_key, + "node_name": item.node_name, + "versions_id": item.versions_id, + "timeout_seconds": item.timeout_seconds, + "retry_count": item.retry_count, + "retry_interval_sec": item.retry_interval_sec, + "position_x": float(item.position_x), + "position_y": float(item.position_y), + "arguments_json": item.arguments_json or {}, + "env_refs_json": item.env_refs_json or {}, + "created_at": _iso(item.created_at), + "updated_at": _iso(item.updated_at), + "version": { + "versions_id": version.versions_id, + "version_label": version.version_label, + "script_id": version.script_id, + "script_name": script.script_name, + "script_type": script.script_type, + "content_hash": version.content_hash, + "created_at": _iso(version.created_at), + }, + } + + +def edge_payload(item: ScheduleEdges) -> dict[str, Any]: + return { + "edge_id": item.edge_id, + "schedule_id": item.schedule_id, + "source_node_id": item.source_node_id, + "target_node_id": item.target_node_id, + "condition_expr": item.condition_expr, + "created_at": _iso(item.created_at), + } + + +def validate_dag( + nodes: list[ScheduleNodes], + edges: list[ScheduleEdges], +) -> dict[str, Any]: + node_by_id = {item.node_id: item for item in nodes} + indegree = {item.node_id: 0 for item in nodes} + outgoing: dict[str, set[str]] = { + item.node_id: set() + for item in nodes + } + errors: list[dict[str, Any]] = [] + seen_edges: set[tuple[str, str]] = set() + + if not nodes: + errors.append( + { + "code": "DAG_EMPTY", + "message": "schedule must contain at least one node", + } + ) + + for edge in edges: + if ( + edge.source_node_id not in node_by_id + or edge.target_node_id not in node_by_id + ): + errors.append( + { + "code": "DAG_EDGE_NODE_MISSING", + "message": "edge references a node outside the schedule", + "edge_id": edge.edge_id, + } + ) + continue + pair = (edge.source_node_id, edge.target_node_id) + if edge.source_node_id == edge.target_node_id: + errors.append( + { + "code": "DAG_SELF_EDGE", + "message": "a node cannot depend on itself", + "edge_id": edge.edge_id, + } + ) + continue + if pair in seen_edges: + errors.append( + { + "code": "DAG_DUPLICATE_EDGE", + "message": "duplicate directed edge", + "edge_id": edge.edge_id, + } + ) + continue + seen_edges.add(pair) + outgoing[edge.source_node_id].add(edge.target_node_id) + indegree[edge.target_node_id] += 1 + + root_ids = sorted( + (node_id for node_id, degree in indegree.items() if degree == 0), + key=lambda node_id: node_by_id[node_id].node_key, + ) + leaf_ids = sorted( + (node_id for node_id, targets in outgoing.items() if not targets), + key=lambda node_id: node_by_id[node_id].node_key, + ) + queue = [ + (node_by_id[node_id].node_key, node_id) + for node_id in root_ids + ] + heapq.heapify(queue) + remaining_indegree = dict(indegree) + ordered_ids: list[str] = [] + while queue: + _, node_id = heapq.heappop(queue) + ordered_ids.append(node_id) + for target_id in sorted( + outgoing[node_id], + key=lambda value: node_by_id[value].node_key, + ): + remaining_indegree[target_id] -= 1 + if remaining_indegree[target_id] == 0: + heapq.heappush( + queue, + (node_by_id[target_id].node_key, target_id), + ) + + if len(ordered_ids) != len(nodes): + cycle_node_ids = sorted( + ( + node_id + for node_id, degree in remaining_indegree.items() + if degree > 0 + ), + key=lambda node_id: node_by_id[node_id].node_key, + ) + errors.append( + { + "code": "DAG_CYCLE", + "message": "schedule graph contains a directed cycle", + "node_ids": cycle_node_ids, + } + ) + + return { + "valid": not errors, + "node_count": len(nodes), + "edge_count": len(edges), + "root_node_ids": root_ids, + "leaf_node_ids": leaf_ids, + "topological_order": ordered_ids, + "errors": errors, + } + + +async def schedule_row( + schedule_id: str, + context: RequestContext, + session: AsyncSession, + *, + for_update: bool = False, +) -> Schedules: + statement = select(Schedules).where( + Schedules.schedule_id == schedule_id, + Schedules.workspace_id == context.workspace.workspace_id, + Schedules.deleted_at.is_(None), + ) + if for_update: + statement = statement.with_for_update() + item = await session.scalar(statement) + if item is None: + raise HTTPException(status.HTTP_404_NOT_FOUND, "schedule not found") + return item + + +def require_workflow_version(item: Schedules, expected: int) -> None: + if item.workflow_version != expected: + raise HTTPException( + status.HTTP_412_PRECONDITION_FAILED, + detail={ + "code": "WORKFLOW_VERSION_CONFLICT", + "message": "schedule was modified by another request", + "expected": expected, + "current": item.workflow_version, + }, + ) + + +async def graph_rows( + schedule_id: str, + session: AsyncSession, +) -> tuple[ + list[tuple[ScheduleNodes, Versions, Scripts]], + list[ScheduleEdges], +]: + node_rows = ( + await session.execute( + select(ScheduleNodes, Versions, Scripts) + .join( + Versions, + Versions.versions_id == ScheduleNodes.versions_id, + ) + .join(Scripts, Scripts.script_id == Versions.script_id) + .where(ScheduleNodes.schedule_id == schedule_id) + .order_by(ScheduleNodes.created_at, ScheduleNodes.node_key) + ) + ).all() + edges = list( + ( + await session.scalars( + select(ScheduleEdges) + .where(ScheduleEdges.schedule_id == schedule_id) + .order_by(ScheduleEdges.created_at, ScheduleEdges.edge_id) + ) + ).all() + ) + return list(node_rows), edges + + +async def detail_payload( + item: Schedules, + session: AsyncSession, +) -> dict[str, Any]: + node_rows, edges = await graph_rows(item.schedule_id, session) + nodes = [row[0] for row in node_rows] + return { + **schedule_payload( + item, + node_count=len(nodes), + edge_count=len(edges), + ), + "nodes": [ + node_payload(node, version, script) + for node, version, script in node_rows + ], + "edges": [edge_payload(edge) for edge in edges], + "dag_validation": validate_dag(nodes, edges), + } + + +async def accessible_version( + versions_id: str, + context: RequestContext, + session: AsyncSession, +) -> tuple[Versions, Scripts]: + row = ( + await session.execute( + select(Versions, Scripts) + .join(Scripts, Scripts.script_id == Versions.script_id) + .where( + Versions.versions_id == versions_id, + Versions.workspace_id == context.workspace.workspace_id, + ) + ) + ).one_or_none() + if row is None: + raise HTTPException( + status.HTTP_404_NOT_FOUND, + "stable version not found", + ) + version, script = row + if not ( + context.is_admin + or version.created_by == context.user.user_id + or version.visibility in {"workspace", "public"} + ): + raise HTTPException( + status.HTTP_403_FORBIDDEN, + "stable version is not visible to this user", + ) + return version, script + + +def _apply_next_run(item: Schedules) -> None: + _timezone(item.timezone) + if item.trigger_type != "cron": + if item.cron_expression: + raise HTTPException( + status.HTTP_422_UNPROCESSABLE_ENTITY, + "cron_expression is only allowed for cron schedules", + ) + item.cron_expression = None + item.next_run_at = None + return + if not item.cron_expression: + raise HTTPException( + status.HTTP_422_UNPROCESSABLE_ENTITY, + "cron_expression is required for cron schedules", + ) + preview = cron_preview( + item.cron_expression, + item.timezone, + count=1, + ) + item.next_run_at = ( + _mysql_utc( + datetime.fromisoformat(preview["occurrences"][0]["utc_time"]) + ) + if item.enabled + else None + ) + + +async def _require_valid_when_enabled( + item: Schedules, + session: AsyncSession, +) -> None: + if not item.enabled: + return + node_rows, edges = await graph_rows(item.schedule_id, session) + validation = validate_dag([row[0] for row in node_rows], edges) + if not validation["valid"]: + raise HTTPException( + status.HTTP_409_CONFLICT, + detail={ + "code": "DAG_INVALID", + "message": "an enabled schedule must contain a valid DAG", + "validation": validation, + }, + ) + + +@router.post("/api/v1/cron/preview") +async def preview_cron( + payload: CronPreviewRequest, + context: RequestContext = Depends(request_context), +) -> dict[str, Any]: + return { + "request_id": context.request_id, + "data": cron_preview( + payload.cron_expression, + payload.timezone, + count=payload.count, + base_time=payload.base_time, + ), + "meta": {}, + } + + +@router.get("/api/v1/schedule-artifacts") +async def list_schedule_artifacts( + limit: int = Query(default=100, ge=1, le=500), + context: RequestContext = Depends(request_context), + session: AsyncSession = Depends(database_session), +) -> dict[str, Any]: + rows = ( + await session.execute( + select(Versions, Scripts) + .join(Scripts, Scripts.script_id == Versions.script_id) + .where( + Versions.workspace_id == context.workspace.workspace_id, + Versions.schedule_hidden_at.is_(None), + ) + .order_by(Versions.created_at.desc()) + .limit(limit) + ) + ).all() + visible = [ + (version, script) + for version, script in rows + if ( + context.is_admin + or version.created_by == context.user.user_id + or version.visibility in {"workspace", "public"} + ) + ] + data = [ + { + "versions_id": version.versions_id, + "version_label": version.version_label, + "script_id": script.script_id, + "script_name": script.script_name, + "script_type": script.script_type, + "content_hash": version.content_hash, + "file_size_bytes": version.file_size_bytes, + "visibility": version.visibility, + "created_by": version.created_by, + "created_at": _iso(version.created_at), + } + for version, script in visible + ] + return { + "request_id": context.request_id, + "data": data, + "meta": {"count": len(data)}, + } + + +@router.get("/api/v1/schedules") +async def list_schedules( + context: RequestContext = Depends(request_context), + session: AsyncSession = Depends(database_session), +) -> dict[str, Any]: + items = list( + ( + await session.scalars( + select(Schedules) + .where( + Schedules.workspace_id == context.workspace.workspace_id, + Schedules.deleted_at.is_(None), + ) + .order_by(Schedules.updated_at.desc()) + ) + ).all() + ) + ids = [item.schedule_id for item in items] + node_counts: dict[str, int] = {} + edge_counts: dict[str, int] = {} + if ids: + node_counts = { + schedule_id: int(count) + for schedule_id, count in ( + await session.execute( + select( + ScheduleNodes.schedule_id, + func.count(ScheduleNodes.node_id), + ) + .where(ScheduleNodes.schedule_id.in_(ids)) + .group_by(ScheduleNodes.schedule_id) + ) + ).all() + } + edge_counts = { + schedule_id: int(count) + for schedule_id, count in ( + await session.execute( + select( + ScheduleEdges.schedule_id, + func.count(ScheduleEdges.edge_id), + ) + .where(ScheduleEdges.schedule_id.in_(ids)) + .group_by(ScheduleEdges.schedule_id) + ) + ).all() + } + return { + "request_id": context.request_id, + "data": [ + schedule_payload( + item, + node_count=node_counts.get(item.schedule_id, 0), + edge_count=edge_counts.get(item.schedule_id, 0), + ) + for item in items + ], + "meta": {"count": len(items)}, + } + + +@router.post( + "/api/v1/schedules", + status_code=status.HTTP_201_CREATED, +) +async def create_schedule( + payload: CreateScheduleRequest, + context: RequestContext = Depends(request_context), + session: AsyncSession = Depends(database_session), +) -> dict[str, Any]: + duplicate = await session.scalar( + select(Schedules).where( + Schedules.workspace_id == context.workspace.workspace_id, + Schedules.schedule_name == payload.schedule_name, + Schedules.deleted_at.is_(None), + ) + ) + if duplicate is not None: + raise HTTPException( + status.HTTP_409_CONFLICT, + "an active schedule with the same name already exists", + ) + if payload.enabled: + raise HTTPException( + status.HTTP_409_CONFLICT, + "a new empty schedule cannot be enabled", + ) + item = Schedules( + schedule_id=new_ulid(), + workspace_id=context.workspace.workspace_id, + schedule_name=payload.schedule_name, + description=payload.description, + trigger_type=payload.trigger_type, + cron_expression=payload.cron_expression, + timezone=payload.timezone, + enabled=0, + workflow_version=1, + max_concurrency=payload.max_concurrency, + failure_policy=payload.failure_policy, + created_by=context.user.user_id, + updated_by=context.user.user_id, + ) + _apply_next_run(item) + session.add(item) + await session.flush() + await session.refresh(item) + return { + "request_id": context.request_id, + "data": await detail_payload(item, session), + "meta": {}, + } + + +@router.get("/api/v1/schedules/{schedule_id}") +async def get_schedule( + schedule_id: str, + context: RequestContext = Depends(request_context), + session: AsyncSession = Depends(database_session), +) -> dict[str, Any]: + item = await schedule_row(schedule_id, context, session) + return { + "request_id": context.request_id, + "data": await detail_payload(item, session), + "meta": {}, + } + + +@router.put("/api/v1/schedules/{schedule_id}") +@router.patch("/api/v1/schedules/{schedule_id}") +async def update_schedule( + schedule_id: str, + payload: UpdateScheduleRequest, + context: RequestContext = Depends(request_context), + session: AsyncSession = Depends(database_session), +) -> dict[str, Any]: + item = await schedule_row( + schedule_id, + context, + session, + for_update=True, + ) + require_workflow_version(item, payload.workflow_version) + mutable_fields = { + "schedule_name", + "description", + "trigger_type", + "cron_expression", + "timezone", + "enabled", + "max_concurrency", + "failure_policy", + } + if ( + "schedule_name" in payload.model_fields_set + and payload.schedule_name != item.schedule_name + ): + duplicate = await session.scalar( + select(Schedules).where( + Schedules.workspace_id == context.workspace.workspace_id, + Schedules.schedule_name == payload.schedule_name, + Schedules.schedule_id != schedule_id, + Schedules.deleted_at.is_(None), + ) + ) + if duplicate is not None: + raise HTTPException( + status.HTTP_409_CONFLICT, + "an active schedule with the same name already exists", + ) + for field in mutable_fields & payload.model_fields_set: + value = getattr(payload, field) + setattr(item, field, int(value) if field == "enabled" else value) + if ( + "trigger_type" in payload.model_fields_set + and item.trigger_type != "cron" + and "cron_expression" not in payload.model_fields_set + ): + item.cron_expression = None + _apply_next_run(item) + await _require_valid_when_enabled(item, session) + item.updated_by = context.user.user_id + item.workflow_version += 1 + await session.flush() + await session.refresh(item) + return { + "request_id": context.request_id, + "data": await detail_payload(item, session), + "meta": {}, + } + + +@router.delete("/api/v1/schedules/{schedule_id}") +async def delete_schedule( + schedule_id: str, + payload: WorkflowVersionRequest, + context: RequestContext = Depends(request_context), + session: AsyncSession = Depends(database_session), +) -> dict[str, Any]: + item = await schedule_row( + schedule_id, + context, + session, + for_update=True, + ) + require_workflow_version(item, payload.workflow_version) + item.enabled = 0 + item.next_run_at = None + item.deleted_at = _mysql_utc(datetime.now(UTC)) + item.updated_by = context.user.user_id + item.workflow_version += 1 + await session.flush() + return { + "request_id": context.request_id, + "data": { + "schedule_id": item.schedule_id, + "deleted": True, + "workflow_version": item.workflow_version, + }, + "meta": {}, + } + + +@router.post( + "/api/v1/schedules/{schedule_id}/nodes", + status_code=status.HTTP_201_CREATED, +) +async def create_schedule_node( + schedule_id: str, + payload: CreateScheduleNodeRequest, + context: RequestContext = Depends(request_context), + session: AsyncSession = Depends(database_session), +) -> dict[str, Any]: + schedule = await schedule_row( + schedule_id, + context, + session, + for_update=True, + ) + require_workflow_version(schedule, payload.workflow_version) + duplicate = await session.scalar( + select(ScheduleNodes).where( + ScheduleNodes.schedule_id == schedule_id, + ScheduleNodes.node_key == payload.node_key, + ) + ) + if duplicate is not None: + raise HTTPException( + status.HTTP_409_CONFLICT, + "node_key already exists in this schedule", + ) + await accessible_version(payload.versions_id, context, session) + node = ScheduleNodes( + node_id=new_ulid(), + schedule_id=schedule_id, + node_key=payload.node_key, + node_name=payload.node_name, + versions_id=payload.versions_id, + timeout_seconds=payload.timeout_seconds, + retry_count=payload.retry_count, + retry_interval_sec=payload.retry_interval_sec, + position_x=Decimal(str(payload.position_x)), + position_y=Decimal(str(payload.position_y)), + arguments_json=payload.arguments_json, + env_refs_json=payload.env_refs_json, + ) + session.add(node) + schedule.updated_by = context.user.user_id + schedule.workflow_version += 1 + await session.flush() + return { + "request_id": context.request_id, + "data": await detail_payload(schedule, session), + "meta": {"created_node_id": node.node_id}, + } + + +@router.put("/api/v1/schedules/{schedule_id}/nodes/{node_id}") +async def update_schedule_node( + schedule_id: str, + node_id: str, + payload: UpdateScheduleNodeRequest, + context: RequestContext = Depends(request_context), + session: AsyncSession = Depends(database_session), +) -> dict[str, Any]: + schedule = await schedule_row( + schedule_id, + context, + session, + for_update=True, + ) + require_workflow_version(schedule, payload.workflow_version) + node = await session.scalar( + select(ScheduleNodes).where( + ScheduleNodes.node_id == node_id, + ScheduleNodes.schedule_id == schedule_id, + ) + ) + if node is None: + raise HTTPException(status.HTTP_404_NOT_FOUND, "schedule node not found") + if ( + "versions_id" in payload.model_fields_set + and payload.versions_id is not None + ): + await accessible_version(payload.versions_id, context, session) + mutable_fields = { + "node_name", + "versions_id", + "timeout_seconds", + "retry_count", + "retry_interval_sec", + "position_x", + "position_y", + "arguments_json", + "env_refs_json", + } + for field in mutable_fields & payload.model_fields_set: + value = getattr(payload, field) + if field in {"position_x", "position_y"} and value is not None: + value = Decimal(str(value)) + setattr(node, field, value) + schedule.updated_by = context.user.user_id + schedule.workflow_version += 1 + await session.flush() + return { + "request_id": context.request_id, + "data": await detail_payload(schedule, session), + "meta": {"updated_node_id": node.node_id}, + } + + +@router.delete("/api/v1/schedules/{schedule_id}/nodes/{node_id}") +async def delete_schedule_node( + schedule_id: str, + node_id: str, + payload: WorkflowVersionRequest, + context: RequestContext = Depends(request_context), + session: AsyncSession = Depends(database_session), +) -> dict[str, Any]: + schedule = await schedule_row( + schedule_id, + context, + session, + for_update=True, + ) + require_workflow_version(schedule, payload.workflow_version) + node = await session.scalar( + select(ScheduleNodes).where( + ScheduleNodes.node_id == node_id, + ScheduleNodes.schedule_id == schedule_id, + ) + ) + if node is None: + raise HTTPException(status.HTTP_404_NOT_FOUND, "schedule node not found") + has_runs = await session.scalar( + select(func.count()) + .select_from(ScheduleNodeRuns) + .where(ScheduleNodeRuns.node_id == node_id) + ) + if int(has_runs or 0): + raise HTTPException( + status.HTTP_409_CONFLICT, + "a node with execution history cannot be deleted", + ) + await session.execute( + delete(ScheduleEdges).where( + ScheduleEdges.schedule_id == schedule_id, + or_( + ScheduleEdges.source_node_id == node_id, + ScheduleEdges.target_node_id == node_id, + ), + ) + ) + await session.delete(node) + schedule.updated_by = context.user.user_id + schedule.workflow_version += 1 + await session.flush() + return { + "request_id": context.request_id, + "data": await detail_payload(schedule, session), + "meta": {"deleted_node_id": node_id}, + } + + +@router.post( + "/api/v1/schedules/{schedule_id}/edges", + status_code=status.HTTP_201_CREATED, +) +async def create_schedule_edge( + schedule_id: str, + payload: CreateScheduleEdgeRequest, + context: RequestContext = Depends(request_context), + session: AsyncSession = Depends(database_session), +) -> dict[str, Any]: + schedule = await schedule_row( + schedule_id, + context, + session, + for_update=True, + ) + require_workflow_version(schedule, payload.workflow_version) + node_ids = set( + ( + await session.scalars( + select(ScheduleNodes.node_id).where( + ScheduleNodes.schedule_id == schedule_id, + ScheduleNodes.node_id.in_( + [payload.source_node_id, payload.target_node_id] + ), + ) + ) + ).all() + ) + if node_ids != {payload.source_node_id, payload.target_node_id}: + raise HTTPException( + status.HTTP_422_UNPROCESSABLE_ENTITY, + "both edge nodes must belong to the schedule", + ) + duplicate = await session.scalar( + select(ScheduleEdges).where( + ScheduleEdges.schedule_id == schedule_id, + ScheduleEdges.source_node_id == payload.source_node_id, + ScheduleEdges.target_node_id == payload.target_node_id, + ) + ) + if duplicate is not None: + raise HTTPException( + status.HTTP_409_CONFLICT, + "directed edge already exists", + ) + edge = ScheduleEdges( + edge_id=new_ulid(), + schedule_id=schedule_id, + source_node_id=payload.source_node_id, + target_node_id=payload.target_node_id, + condition_expr=payload.condition_expr, + ) + session.add(edge) + await session.flush() + node_rows, edges = await graph_rows(schedule_id, session) + validation = validate_dag([row[0] for row in node_rows], edges) + if not validation["valid"]: + raise HTTPException( + status.HTTP_409_CONFLICT, + detail={ + "code": "DAG_INVALID", + "message": "edge would make the schedule graph invalid", + "validation": validation, + }, + ) + schedule.updated_by = context.user.user_id + schedule.workflow_version += 1 + await session.flush() + return { + "request_id": context.request_id, + "data": await detail_payload(schedule, session), + "meta": {"created_edge_id": edge.edge_id}, + } + + +@router.put("/api/v1/schedules/{schedule_id}/edges/{edge_id}") +async def update_schedule_edge( + schedule_id: str, + edge_id: str, + payload: UpdateScheduleEdgeRequest, + context: RequestContext = Depends(request_context), + session: AsyncSession = Depends(database_session), +) -> dict[str, Any]: + schedule = await schedule_row( + schedule_id, + context, + session, + for_update=True, + ) + require_workflow_version(schedule, payload.workflow_version) + edge = await session.scalar( + select(ScheduleEdges).where( + ScheduleEdges.edge_id == edge_id, + ScheduleEdges.schedule_id == schedule_id, + ) + ) + if edge is None: + raise HTTPException(status.HTTP_404_NOT_FOUND, "schedule edge not found") + edge.condition_expr = payload.condition_expr + schedule.updated_by = context.user.user_id + schedule.workflow_version += 1 + await session.flush() + return { + "request_id": context.request_id, + "data": await detail_payload(schedule, session), + "meta": {"updated_edge_id": edge.edge_id}, + } + + +@router.delete("/api/v1/schedules/{schedule_id}/edges/{edge_id}") +async def delete_schedule_edge( + schedule_id: str, + edge_id: str, + payload: WorkflowVersionRequest, + context: RequestContext = Depends(request_context), + session: AsyncSession = Depends(database_session), +) -> dict[str, Any]: + schedule = await schedule_row( + schedule_id, + context, + session, + for_update=True, + ) + require_workflow_version(schedule, payload.workflow_version) + edge = await session.scalar( + select(ScheduleEdges).where( + ScheduleEdges.edge_id == edge_id, + ScheduleEdges.schedule_id == schedule_id, + ) + ) + if edge is None: + raise HTTPException(status.HTTP_404_NOT_FOUND, "schedule edge not found") + await session.delete(edge) + schedule.updated_by = context.user.user_id + schedule.workflow_version += 1 + await session.flush() + return { + "request_id": context.request_id, + "data": await detail_payload(schedule, session), + "meta": {"deleted_edge_id": edge_id}, + } + + +@router.post("/api/v1/schedules/{schedule_id}/validate") +async def validate_schedule( + schedule_id: str, + context: RequestContext = Depends(request_context), + session: AsyncSession = Depends(database_session), +) -> dict[str, Any]: + item = await schedule_row(schedule_id, context, session) + node_rows, edges = await graph_rows(schedule_id, session) + return { + "request_id": context.request_id, + "data": { + "schedule_id": item.schedule_id, + "workflow_version": item.workflow_version, + **validate_dag([row[0] for row in node_rows], edges), + }, + "meta": {}, + } diff --git a/backend/src/backend/schemas.py b/backend/src/backend/schemas.py new file mode 100644 index 0000000..33c9c01 --- /dev/null +++ b/backend/src/backend/schemas.py @@ -0,0 +1,72 @@ +from __future__ import annotations + +from typing import Literal + +from pydantic import BaseModel, ConfigDict, Field, field_validator + + +class StrictModel(BaseModel): + model_config = ConfigDict(extra="forbid") + + +class CreateResourceUploadRequest(StrictModel): + file_name: str = Field(min_length=1, max_length=255) + content_type: str = Field(min_length=1, max_length=255) + expected_size_bytes: int = Field(ge=0, le=100 * 1024 * 1024) + expected_hash: str | None = Field(default=None, min_length=64, max_length=64) + + @field_validator("expected_hash") + @classmethod + def validate_hash(cls, value: str | None) -> str | None: + if value is None: + return None + normalized = value.lower() + if any(character not in "0123456789abcdef" for character in normalized): + raise ValueError("expected_hash must be SHA-256 hex") + return normalized + + +class CompleteResourceUploadRequest(StrictModel): + resource_name: str = Field(min_length=1, max_length=255) + description: str | None = Field(default=None, max_length=1000) + visibility: Literal["private", "workspace", "public"] = "private" + + +class CreateScriptRequest(StrictModel): + script_name: str = Field(min_length=1, max_length=255) + script_type: Literal["python", "notebook"] + content: str = Field(max_length=10 * 1024 * 1024) + visibility: Literal["private", "workspace", "public"] = "private" + parent_path: str | None = Field(default=None, max_length=1024) + + +class CreateWorkspaceDirectoryRequest(StrictModel): + directory_name: str = Field(min_length=1, max_length=255) + parent_path: str = Field(default="", max_length=1024) + + +class UpdateScriptRequest(StrictModel): + content: str = Field(max_length=10 * 1024 * 1024) + + +class PublishVersionRequest(StrictModel): + source_object_id: str | None = Field( + default=None, + min_length=26, + max_length=26, + ) + release_note: str | None = Field(default=None, max_length=1000) + visibility: Literal["private", "workspace", "public"] = "workspace" + + +class DownloadUrlRequest(StrictModel): + expires_seconds: int = Field(default=300, ge=30, le=3600) + + +class FileLockTokenRequest(StrictModel): + lock_token: str = Field(min_length=32, max_length=256) + + +class CreateJupyterAccessTicketRequest(StrictModel): + edit_session_id: str = Field(min_length=26, max_length=26) + lock_token: str = Field(min_length=32, max_length=256) diff --git a/backend/src/backend/scripts.py b/backend/src/backend/scripts.py new file mode 100644 index 0000000..54f1a11 --- /dev/null +++ b/backend/src/backend/scripts.py @@ -0,0 +1,925 @@ +from __future__ import annotations + +import hashlib +import json +import mimetypes +import os +import shutil +import tempfile +from datetime import UTC, datetime +from pathlib import Path, PurePosixPath +from typing import Any + +from fastapi import ( + APIRouter, + Depends, + Header, + HTTPException, + Query, + Request, + status, +) +from sqlalchemy import func, select +from sqlalchemy.ext.asyncio import AsyncSession + +from common.db.models import ( + EditSessions, + Scripts, + StorageObjects, + Versions, +) +from common.ids import new_ulid +from backend.dependencies import ( + RequestContext, + database_session, + request_context, +) +from backend.schemas import ( + CreateScriptRequest, + CreateWorkspaceDirectoryRequest, + DownloadUrlRequest, + PublishVersionRequest, + UpdateScriptRequest, +) + +router = APIRouter(tags=["scripts"]) + + +def normalize_user_path(value: str, *, allow_empty: bool = True) -> str: + normalized = value.replace("\\", "/").strip().strip("/") + if not normalized and allow_empty: + return "" + pure_path = PurePosixPath(normalized) + if ( + pure_path.is_absolute() + or not pure_path.parts + or any( + part in {"", ".", ".."} or any(ord(char) < 32 for char in part) + for part in pure_path.parts + ) + ): + raise HTTPException( + status.HTTP_422_UNPROCESSABLE_ENTITY, + "invalid workspace path", + ) + return pure_path.as_posix() + + +def safe_directory_name(value: str) -> str: + name = value.strip() + if ( + not name + or name in {".", ".."} + or name.startswith(".") + or "/" in name + or "\\" in name + or any(ord(char) < 32 for char in name) + ): + raise HTTPException( + status.HTTP_422_UNPROCESSABLE_ENTITY, + "invalid directory_name", + ) + return name + + +def user_relative_path(context: RequestContext, child_path: str = "") -> str: + base = f"users/{context.user.username}" + normalized = normalize_user_path(child_path) + return f"{base}/{normalized}" if normalized else base + + +def safe_script_name(value: str, script_type: str) -> str: + name = value.replace("\\", "/").rsplit("/", 1)[-1].strip() + if not name or name in {".", ".."} or any(ord(char) < 32 for char in name): + raise HTTPException( + status.HTTP_422_UNPROCESSABLE_ENTITY, + "invalid script_name", + ) + expected_suffix = ".py" if script_type == "python" else ".ipynb" + if not name.lower().endswith(expected_suffix): + name += expected_suffix + return name + + +def validate_script_content(content: str, script_type: str) -> bytes: + encoded = content.encode("utf-8") + if len(encoded) > 10 * 1024 * 1024: + raise HTTPException( + status.HTTP_413_REQUEST_ENTITY_TOO_LARGE, + "script exceeds 10 MiB", + ) + if script_type == "notebook": + try: + notebook = json.loads(content) + except json.JSONDecodeError as exc: + raise HTTPException( + status.HTTP_422_UNPROCESSABLE_ENTITY, + "notebook content must be valid JSON", + ) from exc + if not isinstance(notebook, dict) or not isinstance( + notebook.get("cells"), + list, + ): + raise HTTPException( + status.HTTP_422_UNPROCESSABLE_ENTITY, + "notebook must contain a cells array", + ) + return encoded + + +def workspace_target( + context: RequestContext, + relative_path: str, +) -> Path: + root = Path( + os.getenv("WORKSPACE_ROOT", "/workspace/workspaces") + ).resolve() + scoped_root = (root / context.workspace.workspace_code).resolve() + pure_path = PurePosixPath(relative_path) + target = (scoped_root / Path(*pure_path.parts)).resolve() + if scoped_root not in target.parents: + raise HTTPException( + status.HTTP_422_UNPROCESSABLE_ENTITY, + "script path escapes workspace root", + ) + return target + + +def apply_workspace_permissions(path: Path, mode: int) -> None: + os.chmod(path, mode) + if os.name != "nt": + shared_gid = int(os.getenv("WORKSPACE_SHARED_GID", "100")) + os.chown(path, -1, shared_gid) + + +def atomic_write(target: Path, content: bytes) -> None: + target.parent.mkdir(parents=True, exist_ok=True) + apply_workspace_permissions(target.parent, 0o2770) + descriptor, temporary_name = tempfile.mkstemp( + prefix=f".{target.name}.", + suffix=".tmp", + dir=target.parent, + ) + try: + with os.fdopen(descriptor, "wb") as handle: + handle.write(content) + handle.flush() + os.fsync(handle.fileno()) + os.replace(temporary_name, target) + apply_workspace_permissions(target, 0o660) + finally: + if os.path.exists(temporary_name): + os.unlink(temporary_name) + + +def script_payload( + script: Scripts, + storage_object: StorageObjects | dict[str, Any], +) -> dict[str, Any]: + if isinstance(storage_object, dict): + relative_path = storage_object.get("relative_path") + content_hash = storage_object.get("content_hash") + size_bytes = storage_object.get("size_bytes", 0) + else: + relative_path = storage_object.relative_path + content_hash = storage_object.content_hash + size_bytes = storage_object.size_bytes + return { + "script_id": script.script_id, + "workspace_id": script.workspace_id, + "current_object_id": script.current_object_id, + "owner_user_id": script.owner_user_id, + "script_name": script.script_name, + "script_type": script.script_type, + "visibility": script.visibility, + "status": script.status, + "relative_path": relative_path, + "content_hash": content_hash, + "size_bytes": size_bytes, + "created_at": script.created_at.isoformat(), + "updated_at": script.updated_at.isoformat(), + } + + +def version_payload(version: Versions) -> dict[str, Any]: + return { + "versions_id": version.versions_id, + "workspace_id": version.workspace_id, + "script_id": version.script_id, + "source_object_id": version.source_object_id, + "artifact_object_id": version.artifact_object_id, + "version_no": version.version_no, + "version_label": version.version_label, + "source_path": version.source_path, + "artifact_path": version.artifact_path, + "content_hash": version.content_hash, + "file_size_bytes": version.file_size_bytes, + "visibility": version.visibility, + "release_note": version.release_note, + "created_by": version.created_by, + "created_at": version.created_at.isoformat(), + } + + +async def get_script_row( + script_id: str, + context: RequestContext, + session: AsyncSession, + *, + for_update: bool = False, +) -> tuple[Scripts, StorageObjects]: + if for_update: + script = await session.scalar( + select(Scripts) + .where( + Scripts.script_id == script_id, + Scripts.workspace_id == context.workspace.workspace_id, + Scripts.status == "active", + ) + .with_for_update() + ) + if script is None: + raise HTTPException( + status.HTTP_404_NOT_FOUND, + "script not found", + ) + storage_object = await session.get( + StorageObjects, + script.current_object_id, + ) + if storage_object is None: + raise HTTPException( + status.HTTP_409_CONFLICT, + "script working-copy metadata is missing", + ) + row = (script, storage_object) + else: + statement = ( + select(Scripts, StorageObjects) + .join( + StorageObjects, + StorageObjects.storage_object_id + == Scripts.current_object_id, + ) + .where( + Scripts.script_id == script_id, + Scripts.workspace_id == context.workspace.workspace_id, + Scripts.status == "active", + ) + ) + row = (await session.execute(statement)).one_or_none() + if row is None: + raise HTTPException(status.HTTP_404_NOT_FOUND, "script not found") + script, storage_object = row + return script, storage_object + + +async def require_no_active_edit_session( + session: AsyncSession, + storage_object_ids: list[str], +) -> None: + if not storage_object_ids: + return + active_session = await session.scalar( + select(EditSessions).where( + EditSessions.storage_object_id.in_(storage_object_ids), + EditSessions.session_status == "active", + EditSessions.expires_at > datetime.now(UTC).replace(tzinfo=None), + ) + ) + if active_session is not None: + raise HTTPException( + status.HTTP_409_CONFLICT, + "file is being edited; end the editing session before deletion", + ) + + +async def create_script_record( + *, + name: str, + script_type: str, + content: bytes, + visibility: str, + parent_path: str | None, + request: Request, + context: RequestContext, + session: AsyncSession, +) -> tuple[Scripts, dict[str, Any]]: + folder = ( + "scripts" if script_type == "python" else "notebooks" + ) if parent_path is None else normalize_user_path(parent_path) + child_path = f"{folder}/{name}" if folder else name + relative_path = user_relative_path(context, child_path) + target = workspace_target(context, relative_path) + + existing_script = await session.scalar( + select(Scripts) + .join( + StorageObjects, + StorageObjects.storage_object_id == Scripts.current_object_id, + ) + .where( + Scripts.workspace_id == context.workspace.workspace_id, + StorageObjects.relative_path == relative_path, + Scripts.status == "active", + ) + ) + if existing_script is not None or target.exists(): + raise HTTPException( + status.HTTP_409_CONFLICT, + "a file with the same path already exists", + ) + + atomic_write(target, content) + storage_data = await request.app.state.storage_client.register_workspace_object( + { + "workspace_id": context.workspace.workspace_id, + "user_id": context.user.user_id, + "relative_path": relative_path, + "usage_type": "working_copy", + "visibility": visibility, + } + ) + object_id = storage_data["storage_object_id"] + script = await session.scalar( + select(Scripts).where(Scripts.current_object_id == object_id) + ) + now = datetime.now(UTC).replace(tzinfo=None) + if script is None: + script = Scripts( + script_id=new_ulid(), + workspace_id=context.workspace.workspace_id, + current_object_id=object_id, + owner_user_id=context.user.user_id, + script_name=name, + script_type=script_type, + visibility=visibility, + status="active", + ) + session.add(script) + else: + script.owner_user_id = context.user.user_id + script.script_name = name + script.script_type = script_type + script.visibility = visibility + script.status = "active" + script.deleted_at = None + script.updated_at = now + await session.flush() + await session.refresh(script) + return script, storage_data + + +@router.post("/api/v1/scripts", status_code=status.HTTP_201_CREATED) +async def create_script( + payload: CreateScriptRequest, + request: Request, + context: RequestContext = Depends(request_context), + session: AsyncSession = Depends(database_session), +) -> dict[str, Any]: + name = safe_script_name(payload.script_name, payload.script_type) + content = validate_script_content(payload.content, payload.script_type) + script, storage_data = await create_script_record( + name=name, + script_type=payload.script_type, + content=content, + visibility=payload.visibility, + parent_path=payload.parent_path, + request=request, + context=context, + session=session, + ) + return { + "request_id": context.request_id, + "data": script_payload(script, storage_data), + "meta": {}, + } + + +@router.post( + "/api/v1/scripts/upload", + status_code=status.HTTP_201_CREATED, +) +async def upload_script( + request: Request, + file_name: str = Query(min_length=1, max_length=255), + parent_path: str = Query(default="", max_length=1024), + visibility: str = Query( + default="workspace", + pattern="^(private|workspace|public)$", + ), + context: RequestContext = Depends(request_context), + session: AsyncSession = Depends(database_session), +) -> dict[str, Any]: + suffix = Path(file_name).suffix.lower() + if suffix not in {".py", ".ipynb"}: + raise HTTPException( + status.HTTP_422_UNPROCESSABLE_ENTITY, + "only .py and .ipynb files can be uploaded", + ) + script_type = "notebook" if suffix == ".ipynb" else "python" + name = safe_script_name(file_name, script_type) + body = await request.body() + if len(body) > 10 * 1024 * 1024: + raise HTTPException( + status.HTTP_413_REQUEST_ENTITY_TOO_LARGE, + "script exceeds 10 MiB", + ) + try: + text = body.decode("utf-8") + except UnicodeDecodeError as exc: + raise HTTPException( + status.HTTP_422_UNPROCESSABLE_ENTITY, + "script must use UTF-8 encoding", + ) from exc + content = validate_script_content(text, script_type) + script, storage_data = await create_script_record( + name=name, + script_type=script_type, + content=content, + visibility=visibility, + parent_path=parent_path, + request=request, + context=context, + session=session, + ) + return { + "request_id": context.request_id, + "data": script_payload(script, storage_data), + "meta": {}, + } + + +@router.get("/api/v1/workspace-tree") +async def get_workspace_tree( + context: RequestContext = Depends(request_context), +) -> dict[str, Any]: + root = workspace_target(context, user_relative_path(context)) + root.mkdir(parents=True, exist_ok=True) + apply_workspace_permissions(root, 0o2770) + directories: list[dict[str, str]] = [] + for current, names, _files in os.walk(root, followlinks=False): + names[:] = sorted( + name + for name in names + if not name.startswith(".") + and not (Path(current) / name).is_symlink() + ) + current_path = Path(current) + if current_path == root: + continue + relative = current_path.relative_to(root).as_posix() + parent = PurePosixPath(relative).parent.as_posix() + directories.append( + { + "path": relative, + "name": current_path.name, + "parent_path": "" if parent == "." else parent, + } + ) + return { + "request_id": context.request_id, + "data": {"directories": directories}, + "meta": {"directory_count": len(directories)}, + } + + +@router.post( + "/api/v1/workspace-directories", + status_code=status.HTTP_201_CREATED, +) +async def create_workspace_directory( + payload: CreateWorkspaceDirectoryRequest, + context: RequestContext = Depends(request_context), +) -> dict[str, Any]: + name = safe_directory_name(payload.directory_name) + parent = normalize_user_path(payload.parent_path) + child_path = f"{parent}/{name}" if parent else name + target = workspace_target(context, user_relative_path(context, child_path)) + parent_target = target.parent + user_root = workspace_target(context, user_relative_path(context)) + if ( + not parent_target.is_dir() + or ( + parent_target != user_root + and user_root not in parent_target.parents + ) + ): + raise HTTPException( + status.HTTP_404_NOT_FOUND, + "parent directory not found", + ) + if target.exists(): + raise HTTPException( + status.HTTP_409_CONFLICT, + "a file or directory with the same path already exists", + ) + target.mkdir(parents=False) + apply_workspace_permissions(target, 0o2770) + return { + "request_id": context.request_id, + "data": { + "path": child_path, + "name": name, + "parent_path": parent, + }, + "meta": {}, + } + + +@router.delete("/api/v1/workspace-directories") +async def delete_workspace_directory( + request: Request, + path: str = Query(min_length=1, max_length=1024), + context: RequestContext = Depends(request_context), + session: AsyncSession = Depends(database_session), +) -> dict[str, Any]: + directory_path = normalize_user_path(path, allow_empty=False) + relative_prefix = user_relative_path(context, directory_path) + target = workspace_target(context, relative_prefix) + if not target.is_dir() or target.is_symlink(): + raise HTTPException( + status.HTTP_404_NOT_FOUND, + "directory not found", + ) + + rows = ( + await session.execute( + select(Scripts, StorageObjects) + .join( + StorageObjects, + StorageObjects.storage_object_id == Scripts.current_object_id, + ) + .where( + Scripts.workspace_id == context.workspace.workspace_id, + Scripts.owner_user_id == context.user.user_id, + Scripts.status == "active", + StorageObjects.relative_path.startswith( + f"{relative_prefix}/" + ), + ) + ) + ).all() + await require_no_active_edit_session( + session, + [script.current_object_id for script, _storage in rows], + ) + for script, _storage in rows: + await request.app.state.storage_client.delete_object( + script.current_object_id + ) + script.status = "deleted" + script.deleted_at = datetime.now(UTC).replace(tzinfo=None) + shutil.rmtree(target) + return { + "request_id": context.request_id, + "data": { + "path": directory_path, + "status": "deleted", + "deleted_scripts": len(rows), + "versions_preserved": True, + }, + "meta": {}, + } + + +@router.get("/api/v1/scripts") +async def list_scripts( + context: RequestContext = Depends(request_context), + session: AsyncSession = Depends(database_session), +) -> dict[str, Any]: + statement = ( + select(Scripts, StorageObjects) + .join( + StorageObjects, + StorageObjects.storage_object_id == Scripts.current_object_id, + ) + .where( + Scripts.workspace_id == context.workspace.workspace_id, + Scripts.status == "active", + ) + .order_by(Scripts.updated_at.desc()) + ) + rows = (await session.execute(statement)).all() + return { + "request_id": context.request_id, + "data": [ + script_payload(script, storage_object) + for script, storage_object in rows + ], + "meta": {"count": len(rows)}, + } + + +@router.get("/api/v1/scripts/{script_id}") +async def get_script( + script_id: str, + context: RequestContext = Depends(request_context), + session: AsyncSession = Depends(database_session), +) -> dict[str, Any]: + script, storage_object = await get_script_row( + script_id, + context, + session, + ) + return { + "request_id": context.request_id, + "data": script_payload(script, storage_object), + "meta": {}, + } + + +@router.put("/api/v1/scripts/{script_id}") +async def update_script( + script_id: str, + payload: UpdateScriptRequest, + request: Request, + context: RequestContext = Depends(request_context), + session: AsyncSession = Depends(database_session), +) -> dict[str, Any]: + script, storage_object = await get_script_row( + script_id, + context, + session, + for_update=True, + ) + if ( + script.owner_user_id != context.user.user_id + and not context.is_admin + ): + raise HTTPException( + status.HTTP_403_FORBIDDEN, + "script can only be changed by its owner or an administrator", + ) + content = validate_script_content(payload.content, script.script_type) + if not storage_object.relative_path: + raise HTTPException( + status.HTTP_409_CONFLICT, + "script has no workspace path", + ) + target = workspace_target(context, storage_object.relative_path) + atomic_write(target, content) + storage_data = await request.app.state.storage_client.register_workspace_object( + { + "workspace_id": context.workspace.workspace_id, + "user_id": script.owner_user_id, + "relative_path": storage_object.relative_path, + "usage_type": "working_copy", + "visibility": script.visibility, + } + ) + storage_object.content_hash = storage_data["content_hash"] + storage_object.size_bytes = storage_data["size_bytes"] + script.updated_at = datetime.now(UTC).replace(tzinfo=None) + return { + "request_id": context.request_id, + "data": script_payload(script, storage_object), + "meta": {}, + } + + +@router.delete("/api/v1/scripts/{script_id}") +async def delete_script( + script_id: str, + request: Request, + context: RequestContext = Depends(request_context), + session: AsyncSession = Depends(database_session), +) -> dict[str, Any]: + script, storage_object = await get_script_row( + script_id, + context, + session, + for_update=True, + ) + if ( + script.owner_user_id != context.user.user_id + and not context.is_admin + ): + raise HTTPException( + status.HTTP_403_FORBIDDEN, + "script can only be deleted by its owner or an administrator", + ) + await require_no_active_edit_session( + session, + [script.current_object_id], + ) + if storage_object.relative_path: + target = workspace_target(context, storage_object.relative_path) + if target.is_file(): + target.unlink() + await request.app.state.storage_client.delete_object( + script.current_object_id + ) + script.status = "deleted" + script.deleted_at = datetime.now(UTC).replace(tzinfo=None) + return { + "request_id": context.request_id, + "data": { + "script_id": script.script_id, + "status": script.status, + "versions_preserved": True, + }, + "meta": {}, + } + + +@router.post( + "/api/v1/scripts/{script_id}/versions", + status_code=status.HTTP_201_CREATED, +) +async def publish_version( + script_id: str, + payload: PublishVersionRequest, + request: Request, + context: RequestContext = Depends(request_context), + session: AsyncSession = Depends(database_session), +) -> dict[str, Any]: + script, source_object = await get_script_row( + script_id, + context, + session, + for_update=True, + ) + if ( + script.owner_user_id != context.user.user_id + and not context.is_admin + ): + raise HTTPException( + status.HTTP_403_FORBIDDEN, + "script can only be published by its owner or an administrator", + ) + if ( + payload.source_object_id + and payload.source_object_id != script.current_object_id + ): + raise HTTPException( + status.HTTP_412_PRECONDITION_FAILED, + "source_object_id is not the current working copy", + ) + if not source_object.relative_path: + raise HTTPException( + status.HTTP_409_CONFLICT, + "script has no workspace path", + ) + target = workspace_target(context, source_object.relative_path) + if not target.is_file(): + raise HTTPException( + status.HTTP_409_CONFLICT, + "script working copy is missing", + ) + content = target.read_bytes() + content_hash = hashlib.sha256(content).hexdigest() + existing = await session.scalar( + select(Versions).where( + Versions.script_id == script.script_id, + Versions.content_hash == content_hash, + ) + ) + if existing is not None: + return { + "request_id": context.request_id, + "data": version_payload(existing), + "meta": {"reused": True}, + } + content_type = ( + mimetypes.guess_type(script.script_name)[0] + or "application/octet-stream" + ) + artifact = await request.app.state.storage_client.create_server_object( + workspace_id=context.workspace.workspace_id, + user_id=context.user.user_id, + usage_type="version_artifact", + file_name=script.script_name, + content_type=content_type, + content=content, + visibility=payload.visibility, + is_immutable=True, + idempotency_key=f"version:{script.script_id}:{content_hash}", + ) + current_max = await session.scalar( + select(func.max(Versions.version_no)).where( + Versions.script_id == script.script_id + ) + ) + version_no = int(current_max or 0) + 1 + version = Versions( + versions_id=new_ulid(), + workspace_id=context.workspace.workspace_id, + script_id=script.script_id, + source_object_id=script.current_object_id, + artifact_object_id=artifact["storage_object_id"], + version_no=version_no, + version_label=f"v{version_no}.0", + source_path=source_object.relative_path, + artifact_path=artifact["storage_uri"], + content_hash=content_hash, + file_size_bytes=len(content), + visibility=payload.visibility, + release_note=payload.release_note, + created_by=context.user.user_id, + ) + session.add(version) + await session.flush() + await session.refresh(version) + return { + "request_id": context.request_id, + "data": version_payload(version), + "meta": {"reused": False}, + } + + +@router.get("/api/v1/scripts/{script_id}/versions") +async def list_versions( + script_id: str, + context: RequestContext = Depends(request_context), + session: AsyncSession = Depends(database_session), +) -> dict[str, Any]: + await get_script_row(script_id, context, session) + versions = ( + await session.scalars( + select(Versions) + .where(Versions.script_id == script_id) + .order_by(Versions.version_no.desc()) + ) + ).all() + return { + "request_id": context.request_id, + "data": [version_payload(version) for version in versions], + "meta": {"count": len(versions)}, + } + + +@router.get("/api/v1/versions/{versions_id}") +async def get_version( + versions_id: str, + context: RequestContext = Depends(request_context), + session: AsyncSession = Depends(database_session), +) -> dict[str, Any]: + version = await session.get(Versions, versions_id) + if ( + version is None + or version.workspace_id != context.workspace.workspace_id + ): + raise HTTPException(status.HTTP_404_NOT_FOUND, "version not found") + return { + "request_id": context.request_id, + "data": version_payload(version), + "meta": {}, + } + + +@router.delete("/api/v1/versions/{versions_id}") +async def delete_version( + versions_id: str, + context: RequestContext = Depends(request_context), + session: AsyncSession = Depends(database_session), +) -> dict[str, Any]: + version = await session.get(Versions, versions_id) + if ( + version is None + or version.workspace_id != context.workspace.workspace_id + ): + raise HTTPException(status.HTTP_404_NOT_FOUND, "稳定版本不存在") + if ( + version.created_by != context.user.user_id + and not context.is_admin + ): + raise HTTPException( + status.HTTP_403_FORBIDDEN, + "仅稳定版本发布者或管理员可以删除", + ) + + version.schedule_hidden_at = datetime.now(UTC).replace(tzinfo=None) + await session.flush() + return { + "request_id": context.request_id, + "data": { + "versions_id": versions_id, + "deleted": True, + "artifact_preserved": True, + }, + "meta": { + "message": "已从调度稳定版本列表移除;稳定版本和历史记录保持不变", + }, + } + + +@router.post("/api/v1/versions/{versions_id}/download-url") +async def version_download_url( + versions_id: str, + payload: DownloadUrlRequest, + request: Request, + context: RequestContext = Depends(request_context), + session: AsyncSession = Depends(database_session), +) -> dict[str, Any]: + version = await session.get(Versions, versions_id) + if ( + version is None + or version.workspace_id != context.workspace.workspace_id + ): + raise HTTPException(status.HTTP_404_NOT_FOUND, "version not found") + data = await request.app.state.storage_client.create_download_url( + version.artifact_object_id, + payload.expires_seconds, + ) + return {"request_id": context.request_id, "data": data, "meta": {}} diff --git a/backend/src/backend/storage_api.py b/backend/src/backend/storage_api.py new file mode 100644 index 0000000..bfd0ffc --- /dev/null +++ b/backend/src/backend/storage_api.py @@ -0,0 +1,681 @@ +from __future__ import annotations + +import asyncio +import base64 +import binascii +import hashlib +import mimetypes +import os +import secrets +from contextlib import asynccontextmanager +from datetime import UTC, datetime, timedelta +from pathlib import Path, PurePosixPath +from typing import Any, AsyncIterator + +from fastapi import Depends, Header, HTTPException, Request, status +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from common.db import create_database_engine, create_session_factory +from common.db.models import ( + StorageObjects, + UploadSessions, + Users, + WorkspaceMembers, + Workspaces, +) +from common.ids import new_ulid +from common.service_app import create_service_app +from common.storage import RustFSObjectStore +from backend.storage_schemas import ( + CompleteUploadRequest, + CreateUploadRequest, + DownloadUrlRequest, + RegisterWorkspaceObjectRequest, + ServerObjectRequest, +) + + +def utcnow() -> datetime: + return datetime.now(UTC).replace(tzinfo=None) + + +def hash_bytes(value: str) -> bytes: + return hashlib.sha256(value.encode("utf-8")).digest() + + +def normalized_idempotency_key( + workspace_id: str, + user_id: str, + value: str, +) -> str: + digest = hashlib.sha256( + f"{workspace_id}:{user_id}:{value}".encode("utf-8") + ).hexdigest() + return f"v1:{digest}" + + +def safe_file_name(value: str) -> str: + name = value.replace("\\", "/").rsplit("/", 1)[-1].strip() + if not name or name in {".", ".."} or any(ord(char) < 32 for char in name): + raise HTTPException(status.HTTP_422_UNPROCESSABLE_ENTITY, "invalid file_name") + return name + + +def storage_payload(item: StorageObjects) -> dict[str, Any]: + return { + "storage_object_id": item.storage_object_id, + "workspace_id": item.workspace_id, + "owner_user_id": item.owner_user_id, + "storage_backend": item.storage_backend, + "usage_type": item.usage_type, + "storage_uri": item.storage_uri, + "relative_path": item.relative_path, + "file_name": item.file_name, + "file_extension": item.file_extension, + "mime_type": item.mime_type, + "size_bytes": item.size_bytes, + "content_hash": item.content_hash, + "visibility": item.visibility, + "is_immutable": bool(item.is_immutable), + "object_status": item.object_status, + "created_at": item.created_at.isoformat(), + "updated_at": item.updated_at.isoformat(), + } + + +@asynccontextmanager +async def lifespan(app: Any) -> AsyncIterator[None]: + database_url = os.environ["DATABASE_URL"] + engine = create_database_engine(database_url) + app.state.session_factory = create_session_factory(engine) + app.state.object_store = RustFSObjectStore( + internal_endpoint=os.getenv( + "RUSTFS_INTERNAL_ENDPOINT", + "http://rustfs:9000", + ), + public_endpoint=os.getenv( + "RUSTFS_PUBLIC_ENDPOINT", + "http://localhost:9000", + ), + access_key=os.environ["RUSTFS_ACCESS_KEY"], + secret_key=os.environ["RUSTFS_SECRET_KEY"], + ) + app.state.default_bucket = os.getenv( + "RUSTFS_DEFAULT_BUCKET", + "model-platform", + ) + await asyncio.to_thread( + app.state.object_store.ensure_bucket, + app.state.default_bucket, + ) + try: + yield + finally: + await engine.dispose() + + +app = create_service_app( + os.getenv("SERVICE_NAME", "storage-api"), + lifespan=lifespan, +) + + +async def database_session(request: Request) -> AsyncIterator[AsyncSession]: + async with request.app.state.session_factory() as session: + try: + yield session + await session.commit() + except Exception: + await session.rollback() + raise + + +def verify_internal_service( + x_service_token: str = Header(alias="X-Service-Token"), +) -> None: + expected = os.environ.get("INTERNAL_SERVICE_TOKEN", "") + if not expected or not secrets.compare_digest(expected, x_service_token): + raise HTTPException( + status.HTTP_401_UNAUTHORIZED, + "invalid internal service identity", + ) + + +async def require_workspace_member( + session: AsyncSession, + workspace_id: str, + user_id: str, +) -> Workspaces: + statement = ( + select(Workspaces) + .join( + WorkspaceMembers, + WorkspaceMembers.workspace_id == Workspaces.workspace_id, + ) + .join(Users, Users.user_id == WorkspaceMembers.user_id) + .where( + Workspaces.workspace_id == workspace_id, + Workspaces.status == "active", + WorkspaceMembers.user_id == user_id, + WorkspaceMembers.member_status == "active", + Users.status == "active", + ) + ) + workspace = await session.scalar(statement) + if workspace is None: + raise HTTPException( + status.HTTP_403_FORBIDDEN, + "user is not an active workspace member", + ) + return workspace + + +async def create_upload_record( + payload: CreateUploadRequest, + session: AsyncSession, + request: Request, +) -> dict[str, Any]: + workspace = await require_workspace_member( + session, + payload.workspace_id, + payload.user_id, + ) + stored_key = normalized_idempotency_key( + payload.workspace_id, + payload.user_id, + payload.idempotency_key, + ) + existing = await session.scalar( + select(UploadSessions).where( + UploadSessions.idempotency_key == stored_key + ) + ) + if existing is not None: + if ( + existing.workspace_id != payload.workspace_id + or existing.user_id != payload.user_id + or existing.expected_size_bytes != payload.expected_size_bytes + or existing.expected_hash != payload.expected_hash + or existing.content_type != payload.content_type + ): + raise HTTPException( + status.HTTP_409_CONFLICT, + "idempotency key was used with different upload metadata", + ) + upload = existing + else: + upload_id = new_ulid() + file_name = safe_file_name(payload.file_name) + bucket_name = ( + workspace.artifact_bucket or request.app.state.default_bucket + ) + object_key = ( + f"workspaces/{payload.workspace_id}/" + f"{payload.usage_type}/{upload_id}/{file_name}" + ) + upload = UploadSessions( + upload_id=upload_id, + workspace_id=payload.workspace_id, + user_id=payload.user_id, + idempotency_key=stored_key, + bucket_name=bucket_name, + object_key=object_key, + object_key_hash=hash_bytes(object_key), + upload_status="created", + expires_at=utcnow() + timedelta(minutes=15), + expected_size_bytes=payload.expected_size_bytes, + expected_hash=payload.expected_hash, + content_type=payload.content_type, + ) + session.add(upload) + await session.flush() + + if upload.upload_status == "completed" and upload.storage_object_id: + storage_object = await session.get( + StorageObjects, + upload.storage_object_id, + ) + return { + "upload_id": upload.upload_id, + "status": upload.upload_status, + "storage_object": ( + storage_payload(storage_object) if storage_object else None + ), + } + if upload.upload_status not in {"created", "uploading"}: + raise HTTPException( + status.HTTP_409_CONFLICT, + f"upload cannot continue from status {upload.upload_status}", + ) + + url, headers = request.app.state.object_store.presign_put( + bucket_name=upload.bucket_name, + object_key=upload.object_key, + content_type=upload.content_type or "application/octet-stream", + expected_hash=upload.expected_hash, + expires_seconds=900, + public=payload.url_scope == "public", + ) + return { + "upload_id": upload.upload_id, + "status": upload.upload_status, + "method": "PUT", + "presigned_url": url, + "required_headers": headers, + "expires_at": upload.expires_at.isoformat(), + } + + +async def complete_upload_record( + upload_id: str, + payload: CompleteUploadRequest, + session: AsyncSession, + request: Request, +) -> StorageObjects: + upload = await session.scalar( + select(UploadSessions) + .where(UploadSessions.upload_id == upload_id) + .with_for_update() + ) + if upload is None: + raise HTTPException(status.HTTP_404_NOT_FOUND, "upload not found") + if upload.upload_status == "completed" and upload.storage_object_id: + item = await session.get(StorageObjects, upload.storage_object_id) + if item is None: + raise HTTPException( + status.HTTP_409_CONFLICT, + "completed upload has no storage object", + ) + return item + if upload.upload_status not in {"created", "uploading"}: + raise HTTPException( + status.HTTP_409_CONFLICT, + f"upload cannot be completed from status {upload.upload_status}", + ) + if upload.expires_at < utcnow(): + upload.upload_status = "expired" + raise HTTPException(status.HTTP_409_CONFLICT, "upload expired") + + try: + head = await asyncio.to_thread( + request.app.state.object_store.head, + bucket_name=upload.bucket_name, + object_key=upload.object_key, + ) + except Exception as exc: + raise HTTPException( + status.HTTP_409_CONFLICT, + "uploaded object is not available", + ) from exc + + actual_size = int(head.get("ContentLength", 0)) + if ( + upload.expected_size_bytes is not None + and actual_size != upload.expected_size_bytes + ): + upload.upload_status = "failed" + raise HTTPException( + status.HTTP_409_CONFLICT, + "uploaded object size does not match expected_size_bytes", + ) + actual_content_type = str( + head.get("ContentType") or "application/octet-stream" + ) + if upload.content_type and actual_content_type != upload.content_type: + upload.upload_status = "failed" + raise HTTPException( + status.HTTP_409_CONFLICT, + "uploaded object content type does not match", + ) + metadata = { + str(key).lower(): str(value).lower() + for key, value in dict(head.get("Metadata") or {}).items() + } + actual_hash = metadata.get("sha256") + if not actual_hash: + actual_hash = await asyncio.to_thread( + request.app.state.object_store.sha256, + bucket_name=upload.bucket_name, + object_key=upload.object_key, + ) + if upload.expected_hash and actual_hash != upload.expected_hash: + upload.upload_status = "failed" + raise HTTPException( + status.HTTP_409_CONFLICT, + "uploaded object hash does not match expected_hash", + ) + + file_name = upload.object_key.rsplit("/", 1)[-1] + item = StorageObjects( + storage_object_id=new_ulid(), + workspace_id=upload.workspace_id, + owner_user_id=upload.user_id, + object_type="file", + usage_type=payload.usage_type, + storage_backend="rustfs", + bucket_name=upload.bucket_name, + object_key=upload.object_key, + object_key_hash=upload.object_key_hash, + storage_uri=f"s3://{upload.bucket_name}/{upload.object_key}", + file_name=file_name, + file_extension=Path(file_name).suffix.lower() or None, + mime_type=actual_content_type, + size_bytes=actual_size, + content_hash=actual_hash, + object_etag=str(head.get("ETag", "")).strip('"') or None, + visibility=payload.visibility, + is_immutable=int(payload.is_immutable), + object_status="available", + created_by=upload.user_id, + ) + session.add(item) + await session.flush() + await session.refresh(item) + upload.storage_object_id = item.storage_object_id + upload.upload_status = "completed" + upload.completed_at = utcnow() + return item + + +@app.post("/internal/v1/uploads", dependencies=[Depends(verify_internal_service)]) +async def create_upload( + payload: CreateUploadRequest, + request: Request, + session: AsyncSession = Depends(database_session), +) -> dict[str, Any]: + return { + "data": await create_upload_record(payload, session, request), + } + + +@app.post( + "/internal/v1/uploads/{upload_id}/complete", + dependencies=[Depends(verify_internal_service)], +) +async def complete_upload( + upload_id: str, + payload: CompleteUploadRequest, + request: Request, + session: AsyncSession = Depends(database_session), +) -> dict[str, Any]: + item = await complete_upload_record( + upload_id, + payload, + session, + request, + ) + return {"data": storage_payload(item)} + + +@app.post( + "/internal/v1/uploads/{upload_id}/abort", + dependencies=[Depends(verify_internal_service)], +) +async def abort_upload( + upload_id: str, + request: Request, + session: AsyncSession = Depends(database_session), +) -> dict[str, Any]: + upload = await session.scalar( + select(UploadSessions) + .where(UploadSessions.upload_id == upload_id) + .with_for_update() + ) + if upload is None: + raise HTTPException(status.HTTP_404_NOT_FOUND, "upload not found") + if upload.upload_status == "completed": + raise HTTPException( + status.HTTP_409_CONFLICT, + "completed upload cannot be aborted", + ) + if upload.upload_status != "aborted": + await asyncio.to_thread( + request.app.state.object_store.delete, + bucket_name=upload.bucket_name, + object_key=upload.object_key, + ) + upload.upload_status = "aborted" + return {"data": {"upload_id": upload_id, "status": "aborted"}} + + +@app.post( + "/internal/v1/objects", + dependencies=[Depends(verify_internal_service)], +) +async def create_server_object( + payload: ServerObjectRequest, + request: Request, + session: AsyncSession = Depends(database_session), +) -> dict[str, Any]: + try: + content = base64.b64decode(payload.content_base64, validate=True) + except (binascii.Error, ValueError) as exc: + raise HTTPException( + status.HTTP_422_UNPROCESSABLE_ENTITY, + "content_base64 is invalid", + ) from exc + if len(content) > 100 * 1024 * 1024: + raise HTTPException( + status.HTTP_413_REQUEST_ENTITY_TOO_LARGE, + "object exceeds 100 MiB server-side upload limit", + ) + content_hash = hashlib.sha256(content).hexdigest() + upload_result = await create_upload_record( + CreateUploadRequest( + workspace_id=payload.workspace_id, + user_id=payload.user_id, + usage_type=payload.usage_type, + file_name=payload.file_name, + content_type=payload.content_type, + expected_size_bytes=len(content), + expected_hash=content_hash, + idempotency_key=payload.idempotency_key, + url_scope="internal", + ), + session, + request, + ) + if upload_result.get("status") == "completed": + return {"data": upload_result["storage_object"], "meta": {"reused": True}} + + upload = await session.get(UploadSessions, upload_result["upload_id"]) + if upload is None: + raise HTTPException( + status.HTTP_500_INTERNAL_SERVER_ERROR, + "upload record disappeared", + ) + await asyncio.to_thread( + request.app.state.object_store.put_bytes, + bucket_name=upload.bucket_name, + object_key=upload.object_key, + content=content, + content_type=payload.content_type, + content_hash=content_hash, + ) + item = await complete_upload_record( + upload.upload_id, + CompleteUploadRequest( + usage_type=payload.usage_type, + visibility=payload.visibility, + is_immutable=payload.is_immutable, + ), + session, + request, + ) + return {"data": storage_payload(item), "meta": {"reused": False}} + + +@app.post( + "/internal/v1/workspace-objects", + dependencies=[Depends(verify_internal_service)], +) +async def register_workspace_object( + payload: RegisterWorkspaceObjectRequest, + session: AsyncSession = Depends(database_session), +) -> dict[str, Any]: + workspace = await require_workspace_member( + session, + payload.workspace_id, + payload.user_id, + ) + pure_path = PurePosixPath(payload.relative_path.replace("\\", "/")) + if ( + pure_path.is_absolute() + or not pure_path.parts + or any(part in {"", ".", ".."} for part in pure_path.parts) + ): + raise HTTPException( + status.HTTP_422_UNPROCESSABLE_ENTITY, + "invalid workspace relative_path", + ) + relative_path = pure_path.as_posix() + workspace_root = Path( + os.getenv("WORKSPACE_ROOT", "/workspace/workspaces") + ).resolve() + scoped_root = (workspace_root / workspace.workspace_code).resolve() + target = (scoped_root / Path(*pure_path.parts)).resolve() + if scoped_root != target and scoped_root not in target.parents: + raise HTTPException( + status.HTTP_422_UNPROCESSABLE_ENTITY, + "workspace path escapes its root", + ) + if not target.is_file(): + raise HTTPException( + status.HTTP_404_NOT_FOUND, + "workspace file does not exist", + ) + content = await asyncio.to_thread(target.read_bytes) + content_hash = hashlib.sha256(content).hexdigest() + stat_result = target.stat() + path_digest = hash_bytes(relative_path) + item = await session.scalar( + select(StorageObjects).where( + StorageObjects.workspace_id == payload.workspace_id, + StorageObjects.storage_backend == "workspace_fs", + StorageObjects.path_hash == path_digest, + ) + ) + reused = item is not None + if item is None: + item = StorageObjects( + storage_object_id=new_ulid(), + workspace_id=payload.workspace_id, + owner_user_id=payload.user_id, + object_type="file", + usage_type=payload.usage_type, + storage_backend="workspace_fs", + relative_path=relative_path, + path_hash=path_digest, + storage_uri=target.as_uri(), + file_name=target.name, + visibility=payload.visibility, + is_immutable=0, + object_status="available", + created_by=payload.user_id, + ) + session.add(item) + elif item.is_immutable: + raise HTTPException( + status.HTTP_409_CONFLICT, + "immutable workspace object cannot be updated", + ) + elif item.owner_user_id != payload.user_id: + raise HTTPException( + status.HTTP_403_FORBIDDEN, + "workspace object belongs to another user", + ) + item.usage_type = payload.usage_type + item.file_name = target.name + item.file_extension = target.suffix.lower() or None + item.mime_type = ( + mimetypes.guess_type(target.name)[0] or "application/octet-stream" + ) + item.size_bytes = stat_result.st_size + item.content_hash = content_hash + item.visibility = payload.visibility + item.object_status = "available" + await session.flush() + await session.refresh(item) + return {"data": storage_payload(item), "meta": {"reused": reused}} + + +@app.post( + "/internal/v1/objects/{storage_object_id}/download-url", + dependencies=[Depends(verify_internal_service)], +) +async def create_download_url( + storage_object_id: str, + payload: DownloadUrlRequest, + request: Request, + session: AsyncSession = Depends(database_session), +) -> dict[str, Any]: + item = await session.get(StorageObjects, storage_object_id) + if item is None or item.object_status != "available": + raise HTTPException(status.HTTP_404_NOT_FOUND, "object not found") + if ( + item.storage_backend != "rustfs" + or not item.bucket_name + or not item.object_key + ): + raise HTTPException( + status.HTTP_409_CONFLICT, + "object does not support a presigned URL", + ) + url = request.app.state.object_store.presign_get( + bucket_name=item.bucket_name, + object_key=item.object_key, + file_name=item.file_name, + expires_seconds=payload.expires_seconds, + ) + return { + "data": { + "storage_object_id": item.storage_object_id, + "presigned_url": url, + "method": "GET", + "expires_in_seconds": payload.expires_seconds, + } + } + + +@app.delete( + "/internal/v1/objects/{storage_object_id}", + dependencies=[Depends(verify_internal_service)], +) +async def delete_object( + storage_object_id: str, + request: Request, + session: AsyncSession = Depends(database_session), +) -> dict[str, Any]: + item = await session.scalar( + select(StorageObjects) + .where(StorageObjects.storage_object_id == storage_object_id) + .with_for_update() + ) + if item is None: + raise HTTPException(status.HTTP_404_NOT_FOUND, "object not found") + if item.is_immutable: + raise HTTPException( + status.HTTP_409_CONFLICT, + "immutable object cannot be deleted", + ) + if item.object_status != "deleted": + if item.storage_backend == "rustfs" and item.bucket_name and item.object_key: + await asyncio.to_thread( + request.app.state.object_store.delete, + bucket_name=item.bucket_name, + object_key=item.object_key, + ) + item.object_status = "deleted" + item.deleted_at = utcnow() + return { + "data": { + "storage_object_id": storage_object_id, + "object_status": item.object_status, + } + } + + +@app.get("/internal/health/storage") +async def internal_health() -> dict[str, str]: + return {"status": "ready", "service": "storage-api"} diff --git a/backend/src/backend/storage_client.py b/backend/src/backend/storage_client.py new file mode 100644 index 0000000..8a57826 --- /dev/null +++ b/backend/src/backend/storage_client.py @@ -0,0 +1,116 @@ +from __future__ import annotations + +import base64 +from typing import Any + +import httpx +from fastapi import HTTPException + + +class StorageClient: + def __init__( + self, + client: httpx.AsyncClient, + service_token: str, + ) -> None: + self.client = client + self.headers = {"X-Service-Token": service_token} + + async def _request( + self, + method: str, + path: str, + *, + payload: dict[str, Any] | None = None, + ) -> dict[str, Any]: + response = await self.client.request( + method, + path, + json=payload, + headers=self.headers, + ) + if response.is_error: + try: + detail = response.json().get("detail", response.text) + except ValueError: + detail = response.text + raise HTTPException(response.status_code, detail) + return response.json() + + async def create_upload( + self, + payload: dict[str, Any], + ) -> dict[str, Any]: + return (await self._request( + "POST", + "/internal/v1/uploads", + payload=payload, + ))["data"] + + async def complete_upload( + self, + upload_id: str, + payload: dict[str, Any], + ) -> dict[str, Any]: + return (await self._request( + "POST", + f"/internal/v1/uploads/{upload_id}/complete", + payload=payload, + ))["data"] + + async def register_workspace_object( + self, + payload: dict[str, Any], + ) -> dict[str, Any]: + return (await self._request( + "POST", + "/internal/v1/workspace-objects", + payload=payload, + ))["data"] + + async def create_server_object( + self, + *, + workspace_id: str, + user_id: str, + usage_type: str, + file_name: str, + content_type: str, + content: bytes, + visibility: str, + is_immutable: bool, + idempotency_key: str, + ) -> dict[str, Any]: + result = await self._request( + "POST", + "/internal/v1/objects", + payload={ + "workspace_id": workspace_id, + "user_id": user_id, + "usage_type": usage_type, + "file_name": file_name, + "content_type": content_type, + "content_base64": base64.b64encode(content).decode("ascii"), + "visibility": visibility, + "is_immutable": is_immutable, + "idempotency_key": idempotency_key, + }, + ) + return result["data"] + + async def create_download_url( + self, + storage_object_id: str, + expires_seconds: int, + ) -> dict[str, Any]: + return (await self._request( + "POST", + f"/internal/v1/objects/{storage_object_id}/download-url", + payload={"expires_seconds": expires_seconds}, + ))["data"] + + async def delete_object(self, storage_object_id: str) -> dict[str, Any]: + return (await self._request( + "DELETE", + f"/internal/v1/objects/{storage_object_id}", + ))["data"] diff --git a/backend/src/backend/storage_schemas.py b/backend/src/backend/storage_schemas.py new file mode 100644 index 0000000..0e5f36f --- /dev/null +++ b/backend/src/backend/storage_schemas.py @@ -0,0 +1,79 @@ +from __future__ import annotations + +from typing import Literal + +from pydantic import BaseModel, ConfigDict, Field, field_validator + + +class StrictModel(BaseModel): + model_config = ConfigDict(extra="forbid") + + +class CreateUploadRequest(StrictModel): + workspace_id: str = Field(min_length=26, max_length=26) + user_id: str = Field(min_length=26, max_length=26) + usage_type: Literal[ + "data_resource", + "version_artifact", + "snapshot", + "run_log", + "run_result", + ] + file_name: str = Field(min_length=1, max_length=255) + content_type: str = Field(min_length=1, max_length=255) + expected_size_bytes: int = Field(ge=0, le=100 * 1024 * 1024) + expected_hash: str | None = Field(default=None, min_length=64, max_length=64) + idempotency_key: str = Field(min_length=8, max_length=128) + url_scope: Literal["public", "internal"] = "public" + + @field_validator("expected_hash") + @classmethod + def validate_hash(cls, value: str | None) -> str | None: + if value is None: + return None + normalized = value.lower() + if any(character not in "0123456789abcdef" for character in normalized): + raise ValueError("expected_hash must be lowercase SHA-256 hex") + return normalized + + +class CompleteUploadRequest(StrictModel): + usage_type: Literal[ + "data_resource", + "version_artifact", + "snapshot", + "run_log", + "run_result", + ] + visibility: Literal["private", "workspace", "public"] = "private" + is_immutable: bool = False + + +class ServerObjectRequest(StrictModel): + workspace_id: str = Field(min_length=26, max_length=26) + user_id: str = Field(min_length=26, max_length=26) + usage_type: Literal[ + "data_resource", + "version_artifact", + "snapshot", + "run_log", + "run_result", + ] + file_name: str = Field(min_length=1, max_length=255) + content_type: str = Field(min_length=1, max_length=255) + content_base64: str = Field(min_length=1) + visibility: Literal["private", "workspace", "public"] = "private" + is_immutable: bool = False + idempotency_key: str = Field(min_length=8, max_length=128) + + +class RegisterWorkspaceObjectRequest(StrictModel): + workspace_id: str = Field(min_length=26, max_length=26) + user_id: str = Field(min_length=26, max_length=26) + relative_path: str = Field(min_length=1, max_length=1024) + usage_type: Literal["working_copy", "public_script"] + visibility: Literal["private", "workspace", "public"] = "private" + + +class DownloadUrlRequest(StrictModel): + expires_seconds: int = Field(default=300, ge=30, le=3600) diff --git a/common/README.md b/common/README.md index ca981d3..261a432 100644 --- a/common/README.md +++ b/common/README.md @@ -1,2 +1,5 @@ -## 初始化 alembic -uv run alembic init src/common/migrations \ No newline at end of file +# Common + +后端公共配置、标识、错误模型、日志和基础工具目录。 + +业务模块不得在本目录外重复定义公共 DTO 或错误码。 diff --git a/common/pyproject.toml b/common/pyproject.toml index 8424d06..a79133e 100644 --- a/common/pyproject.toml +++ b/common/pyproject.toml @@ -1,27 +1,17 @@ [project] name = "common" -version = "0.1.0" -description = "Add your description here" -readme = "README.md" -authors = [ - { name = "tao.chen", email = "93983997+taochen-ct@users.noreply.github.com" } -] +version = "0.2.0" requires-python = ">=3.12" dependencies = [ - "alembic>=1.18.5", - "pydantic-settings>=2.14.2", - "pymysql>=1.2.0", - "sqlalchemy>=2.0.51", + "SQLAlchemy==2.0.51", + "asyncmy==0.2.11", + "boto3>=1.34,<2", + "fastapi==0.116.1", ] [build-system] requires = ["hatchling"] build-backend = "hatchling.build" - [tool.hatch.build.targets.wheel] packages = ["src/common"] - -[[tool.uv.index]] -url = "https://pypi.tuna.tsinghua.edu.cn/simple/" -default = true diff --git a/common/src/common/__init__.py b/common/src/common/__init__.py index a130cb4..ab78a93 100644 --- a/common/src/common/__init__.py +++ b/common/src/common/__init__.py @@ -1,2 +1 @@ -def main() -> None: - print("Hello from common!") +"""Shared backend building blocks.""" diff --git a/common/src/common/db/__init__.py b/common/src/common/db/__init__.py index f0ff52a..10b0d03 100644 --- a/common/src/common/db/__init__.py +++ b/common/src/common/db/__init__.py @@ -1,5 +1,17 @@ -# coding=utf-8 -""" -@Time :2026/7/29 -@Author :tao.chen -""" +"""Shared SQLAlchemy database models and infrastructure.""" + +from common.db.models import Base +from common.db.session import ( + AsyncSessionFactory, + create_database_engine, + create_session_factory, + session_scope, +) + +__all__ = [ + "AsyncSessionFactory", + "Base", + "create_database_engine", + "create_session_factory", + "session_scope", +] diff --git a/common/src/common/db/models.py b/common/src/common/db/models.py new file mode 100644 index 0000000..e097c12 --- /dev/null +++ b/common/src/common/db/models.py @@ -0,0 +1,878 @@ +from typing import Optional +import datetime +import decimal + +from sqlalchemy import BINARY, BigInteger, CHAR, DECIMAL, Double, ForeignKeyConstraint, Index, Integer, JSON, String, Text, text +from sqlalchemy.dialects.mysql import BIGINT, DATETIME, INTEGER, SMALLINT, TINYINT +from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship + +class Base(DeclarativeBase): + pass + + +class ConsumerInbox(Base): + __tablename__ = 'consumer_inbox' + __table_args__ = ( + Index('idx_consumer_inbox_status', 'consumer_name', 'process_status', 'created_at'), + {'comment': '消费者幂等 Inbox,防止 Stream 重投导致重复执行'} + ) + + consumer_name: Mapped[str] = mapped_column(String(128), primary_key=True) + event_id: Mapped[str] = mapped_column(CHAR(26), primary_key=True) + process_status: Mapped[str] = mapped_column(String(16), nullable=False, server_default=text("'processing'"), comment='processing/succeeded/failed') + created_at: Mapped[datetime.datetime] = mapped_column(DATETIME(fsp=3), nullable=False, server_default=text('CURRENT_TIMESTAMP(3)')) + message_id: Mapped[Optional[str]] = mapped_column(String(128), comment='Redis Stream message ID') + processed_at: Mapped[Optional[datetime.datetime]] = mapped_column(DATETIME(fsp=3)) + error_message: Mapped[Optional[str]] = mapped_column(String(2000)) + + +class OutboxEvents(Base): + __tablename__ = 'outbox_events' + __table_args__ = ( + Index('idx_outbox_aggregate', 'aggregate_type', 'aggregate_id', 'created_at'), + Index('idx_outbox_idempotency', 'idempotency_key'), + Index('idx_outbox_pending', 'event_status', 'available_at', 'created_at'), + {'comment': '事务 Outbox;提交后发布到 Redis Streams'} + ) + + event_id: Mapped[str] = mapped_column(CHAR(26), primary_key=True) + aggregate_type: Mapped[str] = mapped_column(String(64), nullable=False) + aggregate_id: Mapped[str] = mapped_column(String(128), nullable=False) + event_type: Mapped[str] = mapped_column(String(128), nullable=False) + schema_version: Mapped[int] = mapped_column(SMALLINT, nullable=False, server_default=text("1")) + payload_json: Mapped[dict] = mapped_column(JSON, nullable=False) + event_status: Mapped[str] = mapped_column(String(16), nullable=False, server_default=text("'pending'"), comment='pending/published/failed') + available_at: Mapped[datetime.datetime] = mapped_column(DATETIME(fsp=3), nullable=False, server_default=text('CURRENT_TIMESTAMP(3)')) + retry_count: Mapped[int] = mapped_column(INTEGER, nullable=False, server_default=text("0")) + created_at: Mapped[datetime.datetime] = mapped_column(DATETIME(fsp=3), nullable=False, server_default=text('CURRENT_TIMESTAMP(3)')) + trace_id: Mapped[Optional[str]] = mapped_column(String(64)) + idempotency_key: Mapped[Optional[str]] = mapped_column(String(128)) + published_at: Mapped[Optional[datetime.datetime]] = mapped_column(DATETIME(fsp=3)) + last_error: Mapped[Optional[str]] = mapped_column(String(2000)) + + +class Permissions(Base): + __tablename__ = 'permissions' + __table_args__ = ( + Index('idx_permissions_module', 'module_code'), + Index('uk_permissions_code', 'permission_code', unique=True), + {'comment': '权限点'} + ) + + permission_id: Mapped[str] = mapped_column(CHAR(26), primary_key=True) + permission_code: Mapped[str] = mapped_column(String(128), nullable=False) + permission_name: Mapped[str] = mapped_column(String(100), nullable=False) + module_code: Mapped[str] = mapped_column(String(64), nullable=False) + created_at: Mapped[datetime.datetime] = mapped_column(DATETIME(fsp=3), nullable=False, server_default=text('CURRENT_TIMESTAMP(3)')) + description: Mapped[Optional[str]] = mapped_column(String(500)) + + role_permissions: Mapped[list['RolePermissions']] = relationship('RolePermissions', back_populates='permission') + + +class Roles(Base): + __tablename__ = 'roles' + __table_args__ = ( + Index('uk_roles_code', 'role_code', unique=True), + {'comment': '角色'} + ) + + role_id: Mapped[str] = mapped_column(CHAR(26), primary_key=True) + role_code: Mapped[str] = mapped_column(String(64), nullable=False) + role_name: Mapped[str] = mapped_column(String(100), nullable=False) + role_scope: Mapped[str] = mapped_column(String(16), nullable=False, comment='platform/workspace') + is_builtin: Mapped[int] = mapped_column(TINYINT(1), nullable=False, server_default=text("0")) + created_at: Mapped[datetime.datetime] = mapped_column(DATETIME(fsp=3), nullable=False, server_default=text('CURRENT_TIMESTAMP(3)')) + updated_at: Mapped[datetime.datetime] = mapped_column(DATETIME(fsp=3), nullable=False, server_default=text('CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)')) + description: Mapped[Optional[str]] = mapped_column(String(500)) + + role_permissions: Mapped[list['RolePermissions']] = relationship('RolePermissions', back_populates='role') + users: Mapped[list['Users']] = relationship('Users', back_populates='platform_role') + workspace_members: Mapped[list['WorkspaceMembers']] = relationship('WorkspaceMembers', back_populates='role') + + +class RolePermissions(Base): + __tablename__ = 'role_permissions' + __table_args__ = ( + ForeignKeyConstraint(['permission_id'], ['permissions.permission_id'], ondelete='CASCADE', name='fk_role_permissions_permission'), + ForeignKeyConstraint(['role_id'], ['roles.role_id'], ondelete='CASCADE', name='fk_role_permissions_role'), + Index('fk_role_permissions_permission', 'permission_id'), + {'comment': '角色权限'} + ) + + role_id: Mapped[str] = mapped_column(CHAR(26), primary_key=True) + permission_id: Mapped[str] = mapped_column(CHAR(26), primary_key=True) + created_at: Mapped[datetime.datetime] = mapped_column(DATETIME(fsp=3), nullable=False, server_default=text('CURRENT_TIMESTAMP(3)')) + + permission: Mapped['Permissions'] = relationship('Permissions', back_populates='role_permissions') + role: Mapped['Roles'] = relationship('Roles', back_populates='role_permissions') + + +class Users(Base): + __tablename__ = 'users' + __table_args__ = ( + ForeignKeyConstraint(['platform_role_id'], ['roles.role_id'], ondelete='SET NULL', name='fk_users_platform_role'), + Index('fk_users_platform_role', 'platform_role_id'), + Index('idx_users_status', 'status'), + Index('uk_users_email', 'email', unique=True), + Index('uk_users_username', 'username', unique=True), + {'comment': '平台用户'} + ) + + user_id: Mapped[str] = mapped_column(CHAR(26), primary_key=True) + username: Mapped[str] = mapped_column(String(64), nullable=False) + display_name: Mapped[str] = mapped_column(String(100), nullable=False) + password_hash: Mapped[str] = mapped_column(String(255), nullable=False) + status: Mapped[str] = mapped_column(String(16), nullable=False, server_default=text("'active'"), comment='active/disabled/locked') + created_at: Mapped[datetime.datetime] = mapped_column(DATETIME(fsp=3), nullable=False, server_default=text('CURRENT_TIMESTAMP(3)')) + updated_at: Mapped[datetime.datetime] = mapped_column(DATETIME(fsp=3), nullable=False, server_default=text('CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)')) + email: Mapped[Optional[str]] = mapped_column(String(255)) + platform_role_id: Mapped[Optional[str]] = mapped_column(CHAR(26)) + avatar_uri: Mapped[Optional[str]] = mapped_column(String(1000)) + last_login_at: Mapped[Optional[datetime.datetime]] = mapped_column(DATETIME(fsp=3)) + deleted_at: Mapped[Optional[datetime.datetime]] = mapped_column(DATETIME(fsp=3)) + + platform_role: Mapped[Optional['Roles']] = relationship('Roles', back_populates='users') + workspaces: Mapped[list['Workspaces']] = relationship('Workspaces', back_populates='users') + audit_logs: Mapped[list['AuditLogs']] = relationship('AuditLogs', back_populates='actor_user') + runtime_instances: Mapped[list['RuntimeInstances']] = relationship('RuntimeInstances', foreign_keys='[RuntimeInstances.owner_user_id]', back_populates='owner_user') + runtime_instances_: Mapped[list['RuntimeInstances']] = relationship('RuntimeInstances', foreign_keys='[RuntimeInstances.started_by]', back_populates='users') + schedules: Mapped[list['Schedules']] = relationship('Schedules', foreign_keys='[Schedules.created_by]', back_populates='users') + schedules_: Mapped[list['Schedules']] = relationship('Schedules', foreign_keys='[Schedules.updated_by]', back_populates='users_') + storage_objects: Mapped[list['StorageObjects']] = relationship('StorageObjects', foreign_keys='[StorageObjects.created_by]', back_populates='users') + storage_objects_: Mapped[list['StorageObjects']] = relationship('StorageObjects', foreign_keys='[StorageObjects.owner_user_id]', back_populates='owner_user') + workspace_members: Mapped[list['WorkspaceMembers']] = relationship('WorkspaceMembers', back_populates='user') + data_resources: Mapped[list['DataResources']] = relationship('DataResources', back_populates='owner_user') + edit_sessions: Mapped[list['EditSessions']] = relationship('EditSessions', back_populates='user') + schedule_runs: Mapped[list['ScheduleRuns']] = relationship('ScheduleRuns', back_populates='users') + scripts: Mapped[list['Scripts']] = relationship('Scripts', back_populates='owner_user') + upload_sessions: Mapped[list['UploadSessions']] = relationship('UploadSessions', back_populates='user') + workspace_operations: Mapped[list['WorkspaceOperations']] = relationship('WorkspaceOperations', back_populates='users') + notebook_snapshots: Mapped[list['NotebookSnapshots']] = relationship('NotebookSnapshots', back_populates='users') + versions: Mapped[list['Versions']] = relationship('Versions', back_populates='users') + experiments: Mapped[list['Experiments']] = relationship('Experiments', back_populates='owner_user') + + +class Workspaces(Base): + __tablename__ = 'workspaces' + __table_args__ = ( + ForeignKeyConstraint(['created_by'], ['users.user_id'], ondelete='RESTRICT', name='fk_workspaces_created_by'), + Index('fk_workspaces_created_by', 'created_by'), + Index('idx_workspaces_status', 'status'), + Index('uk_workspaces_code', 'workspace_code', unique=True), + {'comment': 'Workspace'} + ) + + workspace_id: Mapped[str] = mapped_column(CHAR(26), primary_key=True) + workspace_code: Mapped[str] = mapped_column(String(64), nullable=False) + workspace_name: Mapped[str] = mapped_column(String(150), nullable=False) + active_root_uri: Mapped[str] = mapped_column(String(1500), nullable=False, comment='活动工作区,建议 NFS/PVC/file URI') + quota_bytes: Mapped[int] = mapped_column(BIGINT, nullable=False, server_default=text("0"), comment='0 表示不限额') + used_bytes: Mapped[int] = mapped_column(BIGINT, nullable=False, server_default=text("0")) + status: Mapped[str] = mapped_column(String(24), nullable=False, server_default=text("'active'"), comment='creating/active/suspended/deleting/deleted') + created_by: Mapped[str] = mapped_column(CHAR(26), nullable=False) + created_at: Mapped[datetime.datetime] = mapped_column(DATETIME(fsp=3), nullable=False, server_default=text('CURRENT_TIMESTAMP(3)')) + updated_at: Mapped[datetime.datetime] = mapped_column(DATETIME(fsp=3), nullable=False, server_default=text('CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)')) + description: Mapped[Optional[str]] = mapped_column(String(1000)) + artifact_bucket: Mapped[Optional[str]] = mapped_column(String(128), comment='RustFS bucket') + artifact_prefix: Mapped[Optional[str]] = mapped_column(String(512), comment='RustFS object key prefix') + deleted_at: Mapped[Optional[datetime.datetime]] = mapped_column(DATETIME(fsp=3)) + + users: Mapped['Users'] = relationship('Users', back_populates='workspaces') + audit_logs: Mapped[list['AuditLogs']] = relationship('AuditLogs', back_populates='workspace') + runtime_instances: Mapped[list['RuntimeInstances']] = relationship('RuntimeInstances', back_populates='workspace') + schedules: Mapped[list['Schedules']] = relationship('Schedules', back_populates='workspace') + storage_objects: Mapped[list['StorageObjects']] = relationship('StorageObjects', back_populates='workspace') + workspace_members: Mapped[list['WorkspaceMembers']] = relationship('WorkspaceMembers', back_populates='workspace') + data_resources: Mapped[list['DataResources']] = relationship('DataResources', back_populates='workspace') + edit_sessions: Mapped[list['EditSessions']] = relationship('EditSessions', back_populates='workspace') + schedule_runs: Mapped[list['ScheduleRuns']] = relationship('ScheduleRuns', back_populates='workspace') + scripts: Mapped[list['Scripts']] = relationship('Scripts', back_populates='workspace') + upload_sessions: Mapped[list['UploadSessions']] = relationship('UploadSessions', back_populates='workspace') + workspace_operations: Mapped[list['WorkspaceOperations']] = relationship('WorkspaceOperations', back_populates='workspace') + notebook_snapshots: Mapped[list['NotebookSnapshots']] = relationship('NotebookSnapshots', back_populates='workspace') + versions: Mapped[list['Versions']] = relationship('Versions', back_populates='workspace') + experiments: Mapped[list['Experiments']] = relationship('Experiments', back_populates='workspace') + + +class AuditLogs(Base): + __tablename__ = 'audit_logs' + __table_args__ = ( + ForeignKeyConstraint(['actor_user_id'], ['users.user_id'], ondelete='SET NULL', name='fk_audit_actor'), + ForeignKeyConstraint(['workspace_id'], ['workspaces.workspace_id'], ondelete='SET NULL', name='fk_audit_workspace'), + Index('idx_audit_action_time', 'action_code', 'created_at'), + Index('idx_audit_actor_time', 'actor_user_id', 'created_at'), + Index('idx_audit_workspace_time', 'workspace_id', 'created_at'), + {'comment': '操作审计日志'} + ) + + audit_id: Mapped[int] = mapped_column(BIGINT, primary_key=True) + action_code: Mapped[str] = mapped_column(String(128), nullable=False) + target_type: Mapped[str] = mapped_column(String(64), nullable=False) + operation_status: Mapped[str] = mapped_column(String(16), nullable=False, server_default=text("'success'")) + created_at: Mapped[datetime.datetime] = mapped_column(DATETIME(fsp=3), nullable=False, server_default=text('CURRENT_TIMESTAMP(3)')) + workspace_id: Mapped[Optional[str]] = mapped_column(CHAR(26)) + actor_user_id: Mapped[Optional[str]] = mapped_column(CHAR(26)) + target_id: Mapped[Optional[str]] = mapped_column(String(128)) + client_ip: Mapped[Optional[str]] = mapped_column(String(45)) + user_agent: Mapped[Optional[str]] = mapped_column(String(1000)) + detail_json: Mapped[Optional[dict]] = mapped_column(JSON) + + actor_user: Mapped[Optional['Users']] = relationship('Users', back_populates='audit_logs') + workspace: Mapped[Optional['Workspaces']] = relationship('Workspaces', back_populates='audit_logs') + + +class RuntimeInstances(Base): + __tablename__ = 'runtime_instances' + __table_args__ = ( + ForeignKeyConstraint(['owner_user_id'], ['users.user_id'], ondelete='SET NULL', name='fk_runtime_owner'), + ForeignKeyConstraint(['started_by'], ['users.user_id'], ondelete='RESTRICT', name='fk_runtime_started_by'), + ForeignKeyConstraint(['workspace_id'], ['workspaces.workspace_id'], ondelete='RESTRICT', name='fk_runtime_workspace'), + Index('fk_runtime_started_by', 'started_by'), + Index('idx_runtime_lease', 'actual_state', 'lease_expires_at'), + Index('idx_runtime_owner_state', 'owner_user_id', 'actual_state'), + Index('idx_runtime_workspace_state', 'workspace_id', 'runtime_type', 'actual_state'), + {'comment': 'Jupyter/未来 VS Code、OpenCode Runtime 实例'} + ) + + runtime_id: Mapped[str] = mapped_column(CHAR(26), primary_key=True) + workspace_id: Mapped[str] = mapped_column(CHAR(26), nullable=False) + runtime_type: Mapped[str] = mapped_column(String(24), nullable=False, server_default=text("'jupyter'")) + runtime_provider: Mapped[str] = mapped_column(String(24), nullable=False, comment='process/docker/kubernetes') + proxy_base_path: Mapped[str] = mapped_column(String(512), nullable=False) + desired_state: Mapped[str] = mapped_column(String(16), nullable=False, server_default=text("'running'")) + actual_state: Mapped[str] = mapped_column(String(24), nullable=False, server_default=text("'provisioning'"), comment='provisioning/starting/running/unhealthy/stopping/stopped/failed') + state_version: Mapped[int] = mapped_column(INTEGER, nullable=False, server_default=text("0"), comment='乐观锁版本') + started_by: Mapped[str] = mapped_column(CHAR(26), nullable=False) + created_at: Mapped[datetime.datetime] = mapped_column(DATETIME(fsp=3), nullable=False, server_default=text('CURRENT_TIMESTAMP(3)')) + updated_at: Mapped[datetime.datetime] = mapped_column(DATETIME(fsp=3), nullable=False, server_default=text('CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)')) + owner_user_id: Mapped[Optional[str]] = mapped_column(CHAR(26), comment='为空表示 Workspace 级 Runtime') + runtime_ref: Mapped[Optional[str]] = mapped_column(String(255), comment='PID/container ID/pod UID') + host_node: Mapped[Optional[str]] = mapped_column(String(255)) + internal_url: Mapped[Optional[str]] = mapped_column(String(1000)) + started_at: Mapped[Optional[datetime.datetime]] = mapped_column(DATETIME(fsp=3)) + last_heartbeat_at: Mapped[Optional[datetime.datetime]] = mapped_column(DATETIME(fsp=3)) + lease_expires_at: Mapped[Optional[datetime.datetime]] = mapped_column(DATETIME(fsp=3)) + stopped_at: Mapped[Optional[datetime.datetime]] = mapped_column(DATETIME(fsp=3)) + error_message: Mapped[Optional[str]] = mapped_column(Text) + + owner_user: Mapped[Optional['Users']] = relationship('Users', foreign_keys=[owner_user_id], back_populates='runtime_instances') + users: Mapped['Users'] = relationship('Users', foreign_keys=[started_by], back_populates='runtime_instances_') + workspace: Mapped['Workspaces'] = relationship('Workspaces', back_populates='runtime_instances') + edit_sessions: Mapped[list['EditSessions']] = relationship('EditSessions', back_populates='runtime') + workspace_operations: Mapped[list['WorkspaceOperations']] = relationship('WorkspaceOperations', back_populates='runtime') + + +class Schedules(Base): + __tablename__ = 'schedules' + __table_args__ = ( + ForeignKeyConstraint(['created_by'], ['users.user_id'], ondelete='RESTRICT', name='fk_schedules_created_by'), + ForeignKeyConstraint(['updated_by'], ['users.user_id'], ondelete='RESTRICT', name='fk_schedules_updated_by'), + ForeignKeyConstraint(['workspace_id'], ['workspaces.workspace_id'], ondelete='RESTRICT', name='fk_schedules_workspace'), + Index('fk_schedules_created_by', 'created_by'), + Index('fk_schedules_updated_by', 'updated_by'), + Index('idx_schedules_due', 'enabled', 'next_run_at'), + Index('idx_schedules_workspace', 'workspace_id', 'enabled', 'updated_at'), + {'comment': '调度方案'} + ) + + schedule_id: Mapped[str] = mapped_column(CHAR(26), primary_key=True) + workspace_id: Mapped[str] = mapped_column(CHAR(26), nullable=False) + schedule_name: Mapped[str] = mapped_column(String(255), nullable=False) + trigger_type: Mapped[str] = mapped_column(String(16), nullable=False, server_default=text("'cron'"), comment='manual/cron/api') + timezone: Mapped[str] = mapped_column(String(64), nullable=False, server_default=text("'Asia/Shanghai'")) + enabled: Mapped[int] = mapped_column(TINYINT(1), nullable=False, server_default=text("0")) + workflow_version: Mapped[int] = mapped_column(INTEGER, nullable=False, server_default=text("1")) + max_concurrency: Mapped[int] = mapped_column(INTEGER, nullable=False, server_default=text("1")) + failure_policy: Mapped[str] = mapped_column(String(24), nullable=False, server_default=text("'stop'"), comment='stop/continue') + created_by: Mapped[str] = mapped_column(CHAR(26), nullable=False) + updated_by: Mapped[str] = mapped_column(CHAR(26), nullable=False) + created_at: Mapped[datetime.datetime] = mapped_column(DATETIME(fsp=3), nullable=False, server_default=text('CURRENT_TIMESTAMP(3)')) + updated_at: Mapped[datetime.datetime] = mapped_column(DATETIME(fsp=3), nullable=False, server_default=text('CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)')) + description: Mapped[Optional[str]] = mapped_column(String(1000)) + cron_expression: Mapped[Optional[str]] = mapped_column(String(128)) + last_run_at: Mapped[Optional[datetime.datetime]] = mapped_column(DATETIME(fsp=3)) + next_run_at: Mapped[Optional[datetime.datetime]] = mapped_column(DATETIME(fsp=3)) + deleted_at: Mapped[Optional[datetime.datetime]] = mapped_column(DATETIME(fsp=3)) + + users: Mapped['Users'] = relationship('Users', foreign_keys=[created_by], back_populates='schedules') + users_: Mapped['Users'] = relationship('Users', foreign_keys=[updated_by], back_populates='schedules_') + workspace: Mapped['Workspaces'] = relationship('Workspaces', back_populates='schedules') + schedule_runs: Mapped[list['ScheduleRuns']] = relationship('ScheduleRuns', back_populates='schedule') + schedule_nodes: Mapped[list['ScheduleNodes']] = relationship('ScheduleNodes', back_populates='schedule') + schedule_edges: Mapped[list['ScheduleEdges']] = relationship('ScheduleEdges', back_populates='schedule') + + +class StorageObjects(Base): + __tablename__ = 'storage_objects' + __table_args__ = ( + ForeignKeyConstraint(['created_by'], ['users.user_id'], ondelete='RESTRICT', name='fk_storage_created_by'), + ForeignKeyConstraint(['owner_user_id'], ['users.user_id'], ondelete='SET NULL', name='fk_storage_owner'), + ForeignKeyConstraint(['parent_object_id'], ['storage_objects.storage_object_id'], ondelete='SET NULL', name='fk_storage_parent'), + ForeignKeyConstraint(['workspace_id'], ['workspaces.workspace_id'], ondelete='RESTRICT', name='fk_storage_workspace'), + Index('fk_storage_created_by', 'created_by'), + Index('idx_storage_content_hash', 'content_hash'), + Index('idx_storage_owner', 'owner_user_id', 'object_status'), + Index('idx_storage_parent', 'parent_object_id'), + Index('idx_storage_workspace_usage', 'workspace_id', 'usage_type', 'object_status'), + Index('uk_storage_bucket_key', 'storage_backend', 'bucket_name', 'object_key_hash', unique=True), + Index('uk_storage_workspace_path', 'workspace_id', 'storage_backend', 'path_hash', unique=True), + {'comment': 'Workspace 文件和 RustFS 对象的统一元数据'} + ) + + storage_object_id: Mapped[str] = mapped_column(CHAR(26), primary_key=True) + workspace_id: Mapped[str] = mapped_column(CHAR(26), nullable=False) + object_type: Mapped[str] = mapped_column(String(16), nullable=False, comment='file/directory') + usage_type: Mapped[str] = mapped_column(String(32), nullable=False, comment='working_copy/public_script/data_resource/version_artifact/snapshot/run_log/run_result') + storage_backend: Mapped[str] = mapped_column(String(16), nullable=False, comment='workspace_fs/rustfs') + storage_uri: Mapped[str] = mapped_column(String(1500), nullable=False) + file_name: Mapped[str] = mapped_column(String(255), nullable=False) + size_bytes: Mapped[int] = mapped_column(BIGINT, nullable=False, server_default=text("0")) + visibility: Mapped[str] = mapped_column(String(16), nullable=False, server_default=text("'private'"), comment='private/workspace/public') + is_immutable: Mapped[int] = mapped_column(TINYINT(1), nullable=False, server_default=text("0")) + object_status: Mapped[str] = mapped_column(String(24), nullable=False, server_default=text("'available'"), comment='uploading/available/deleting/deleted/failed') + created_by: Mapped[str] = mapped_column(CHAR(26), nullable=False) + created_at: Mapped[datetime.datetime] = mapped_column(DATETIME(fsp=3), nullable=False, server_default=text('CURRENT_TIMESTAMP(3)')) + updated_at: Mapped[datetime.datetime] = mapped_column(DATETIME(fsp=3), nullable=False, server_default=text('CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)')) + owner_user_id: Mapped[Optional[str]] = mapped_column(CHAR(26)) + parent_object_id: Mapped[Optional[str]] = mapped_column(CHAR(26)) + relative_path: Mapped[Optional[str]] = mapped_column(String(1024), comment='Workspace 相对路径') + path_hash: Mapped[Optional[bytes]] = mapped_column(BINARY(32), comment='SHA-256(relative_path),由应用写入') + bucket_name: Mapped[Optional[str]] = mapped_column(String(128)) + object_key: Mapped[Optional[str]] = mapped_column(String(1024)) + object_key_hash: Mapped[Optional[bytes]] = mapped_column(BINARY(32), comment='SHA-256(object_key),由应用写入') + file_extension: Mapped[Optional[str]] = mapped_column(String(32)) + mime_type: Mapped[Optional[str]] = mapped_column(String(255)) + content_hash: Mapped[Optional[str]] = mapped_column(CHAR(64), comment='SHA-256 hex') + object_etag: Mapped[Optional[str]] = mapped_column(String(255)) + deleted_at: Mapped[Optional[datetime.datetime]] = mapped_column(DATETIME(fsp=3)) + + users: Mapped['Users'] = relationship('Users', foreign_keys=[created_by], back_populates='storage_objects') + owner_user: Mapped[Optional['Users']] = relationship('Users', foreign_keys=[owner_user_id], back_populates='storage_objects_') + parent_object: Mapped[Optional['StorageObjects']] = relationship('StorageObjects', remote_side=[storage_object_id], back_populates='parent_object_reverse') + parent_object_reverse: Mapped[list['StorageObjects']] = relationship('StorageObjects', remote_side=[parent_object_id], back_populates='parent_object') + workspace: Mapped['Workspaces'] = relationship('Workspaces', back_populates='storage_objects') + data_resources: Mapped[list['DataResources']] = relationship('DataResources', back_populates='storage_object') + edit_sessions: Mapped[list['EditSessions']] = relationship('EditSessions', back_populates='storage_object') + schedule_runs: Mapped[list['ScheduleRuns']] = relationship('ScheduleRuns', foreign_keys='[ScheduleRuns.logs_object_id]', back_populates='logs_object') + schedule_runs_: Mapped[list['ScheduleRuns']] = relationship('ScheduleRuns', foreign_keys='[ScheduleRuns.result_object_id]', back_populates='result_object') + scripts: Mapped[list['Scripts']] = relationship('Scripts', back_populates='current_object') + upload_sessions: Mapped[list['UploadSessions']] = relationship('UploadSessions', back_populates='storage_object') + notebook_snapshots: Mapped[list['NotebookSnapshots']] = relationship('NotebookSnapshots', foreign_keys='[NotebookSnapshots.artifact_object_id]', back_populates='artifact_object') + notebook_snapshots_: Mapped[list['NotebookSnapshots']] = relationship('NotebookSnapshots', foreign_keys='[NotebookSnapshots.source_object_id]', back_populates='source_object') + versions: Mapped[list['Versions']] = relationship('Versions', foreign_keys='[Versions.artifact_object_id]', back_populates='artifact_object') + versions_: Mapped[list['Versions']] = relationship('Versions', foreign_keys='[Versions.source_object_id]', back_populates='source_object') + experiments: Mapped[list['Experiments']] = relationship('Experiments', foreign_keys='[Experiments.logs_object_id]', back_populates='logs_object') + experiments_: Mapped[list['Experiments']] = relationship('Experiments', foreign_keys='[Experiments.result_object_id]', back_populates='result_object') + schedule_node_runs: Mapped[list['ScheduleNodeRuns']] = relationship('ScheduleNodeRuns', foreign_keys='[ScheduleNodeRuns.logs_object_id]', back_populates='logs_object') + schedule_node_runs_: Mapped[list['ScheduleNodeRuns']] = relationship('ScheduleNodeRuns', foreign_keys='[ScheduleNodeRuns.result_object_id]', back_populates='result_object') + + +class WorkspaceMembers(Base): + __tablename__ = 'workspace_members' + __table_args__ = ( + ForeignKeyConstraint(['role_id'], ['roles.role_id'], ondelete='RESTRICT', name='fk_workspace_members_role'), + ForeignKeyConstraint(['user_id'], ['users.user_id'], ondelete='RESTRICT', name='fk_workspace_members_user'), + ForeignKeyConstraint(['workspace_id'], ['workspaces.workspace_id'], ondelete='CASCADE', name='fk_workspace_members_workspace'), + Index('idx_workspace_members_role', 'role_id'), + Index('idx_workspace_members_user', 'user_id', 'member_status'), + {'comment': 'Workspace 成员与角色'} + ) + + workspace_id: Mapped[str] = mapped_column(CHAR(26), primary_key=True) + user_id: Mapped[str] = mapped_column(CHAR(26), primary_key=True) + role_id: Mapped[str] = mapped_column(CHAR(26), nullable=False) + member_status: Mapped[str] = mapped_column(String(16), nullable=False, server_default=text("'active'")) + joined_at: Mapped[datetime.datetime] = mapped_column(DATETIME(fsp=3), nullable=False, server_default=text('CURRENT_TIMESTAMP(3)')) + updated_at: Mapped[datetime.datetime] = mapped_column(DATETIME(fsp=3), nullable=False, server_default=text('CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)')) + + role: Mapped['Roles'] = relationship('Roles', back_populates='workspace_members') + user: Mapped['Users'] = relationship('Users', back_populates='workspace_members') + workspace: Mapped['Workspaces'] = relationship('Workspaces', back_populates='workspace_members') + + +class DataResources(Base): + __tablename__ = 'data_resources' + __table_args__ = ( + ForeignKeyConstraint(['owner_user_id'], ['users.user_id'], ondelete='RESTRICT', name='fk_data_resources_owner'), + ForeignKeyConstraint(['storage_object_id'], ['storage_objects.storage_object_id'], ondelete='RESTRICT', name='fk_data_resources_object'), + ForeignKeyConstraint(['workspace_id'], ['workspaces.workspace_id'], ondelete='RESTRICT', name='fk_data_resources_workspace'), + Index('idx_data_resources_owner', 'owner_user_id', 'status'), + Index('idx_data_resources_workspace', 'workspace_id', 'visibility', 'status'), + Index('uk_data_resources_object', 'storage_object_id', unique=True), + {'comment': '数据资源'} + ) + + resource_id: Mapped[str] = mapped_column(CHAR(26), primary_key=True) + workspace_id: Mapped[str] = mapped_column(CHAR(26), nullable=False) + storage_object_id: Mapped[str] = mapped_column(CHAR(26), nullable=False) + owner_user_id: Mapped[str] = mapped_column(CHAR(26), nullable=False) + resource_name: Mapped[str] = mapped_column(String(255), nullable=False) + visibility: Mapped[str] = mapped_column(String(16), nullable=False, server_default=text("'private'")) + status: Mapped[str] = mapped_column(String(16), nullable=False, server_default=text("'active'")) + created_at: Mapped[datetime.datetime] = mapped_column(DATETIME(fsp=3), nullable=False, server_default=text('CURRENT_TIMESTAMP(3)')) + updated_at: Mapped[datetime.datetime] = mapped_column(DATETIME(fsp=3), nullable=False, server_default=text('CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)')) + description: Mapped[Optional[str]] = mapped_column(String(1000)) + schema_json: Mapped[Optional[dict]] = mapped_column(JSON, comment='字段结构、行数等可选元数据') + deleted_at: Mapped[Optional[datetime.datetime]] = mapped_column(DATETIME(fsp=3)) + + owner_user: Mapped['Users'] = relationship('Users', back_populates='data_resources') + storage_object: Mapped['StorageObjects'] = relationship('StorageObjects', back_populates='data_resources') + workspace: Mapped['Workspaces'] = relationship('Workspaces', back_populates='data_resources') + experiment_resources: Mapped[list['ExperimentResources']] = relationship('ExperimentResources', back_populates='resource') + + +class EditSessions(Base): + __tablename__ = 'edit_sessions' + __table_args__ = ( + ForeignKeyConstraint(['runtime_id'], ['runtime_instances.runtime_id'], ondelete='SET NULL', name='fk_edit_sessions_runtime'), + ForeignKeyConstraint(['storage_object_id'], ['storage_objects.storage_object_id'], ondelete='RESTRICT', name='fk_edit_sessions_object'), + ForeignKeyConstraint(['user_id'], ['users.user_id'], ondelete='RESTRICT', name='fk_edit_sessions_user'), + ForeignKeyConstraint(['workspace_id'], ['workspaces.workspace_id'], ondelete='RESTRICT', name='fk_edit_sessions_workspace'), + Index('fk_edit_sessions_workspace', 'workspace_id'), + Index('idx_edit_sessions_object', 'storage_object_id', 'session_status', 'expires_at'), + Index('idx_edit_sessions_runtime', 'runtime_id', 'session_status'), + Index('idx_edit_sessions_user', 'user_id', 'session_status'), + {'comment': '编辑会话审计;实时锁状态以 Redis 为准'} + ) + + edit_session_id: Mapped[str] = mapped_column(CHAR(26), primary_key=True) + workspace_id: Mapped[str] = mapped_column(CHAR(26), nullable=False) + storage_object_id: Mapped[str] = mapped_column(CHAR(26), nullable=False) + user_id: Mapped[str] = mapped_column(CHAR(26), nullable=False) + redis_lock_key: Mapped[str] = mapped_column(String(512), nullable=False) + lock_token_hash: Mapped[bytes] = mapped_column(BINARY(32), nullable=False, comment='不保存原始 token') + session_status: Mapped[str] = mapped_column(String(16), nullable=False, server_default=text("'active'"), comment='active/closed/expired/failed') + started_at: Mapped[datetime.datetime] = mapped_column(DATETIME(fsp=3), nullable=False, server_default=text('CURRENT_TIMESTAMP(3)')) + last_heartbeat_at: Mapped[datetime.datetime] = mapped_column(DATETIME(fsp=3), nullable=False, server_default=text('CURRENT_TIMESTAMP(3)')) + expires_at: Mapped[datetime.datetime] = mapped_column(DATETIME(fsp=3), nullable=False) + runtime_id: Mapped[Optional[str]] = mapped_column(CHAR(26)) + jupyter_session_id: Mapped[Optional[str]] = mapped_column(String(255)) + ended_at: Mapped[Optional[datetime.datetime]] = mapped_column(DATETIME(fsp=3)) + end_reason: Mapped[Optional[str]] = mapped_column(String(64)) + + runtime: Mapped[Optional['RuntimeInstances']] = relationship('RuntimeInstances', back_populates='edit_sessions') + storage_object: Mapped['StorageObjects'] = relationship('StorageObjects', back_populates='edit_sessions') + user: Mapped['Users'] = relationship('Users', back_populates='edit_sessions') + workspace: Mapped['Workspaces'] = relationship('Workspaces', back_populates='edit_sessions') + + +class ScheduleRuns(Base): + __tablename__ = 'schedule_runs' + __table_args__ = ( + ForeignKeyConstraint(['logs_object_id'], ['storage_objects.storage_object_id'], ondelete='SET NULL', name='fk_schedule_runs_logs'), + ForeignKeyConstraint(['result_object_id'], ['storage_objects.storage_object_id'], ondelete='SET NULL', name='fk_schedule_runs_result'), + ForeignKeyConstraint(['schedule_id'], ['schedules.schedule_id'], ondelete='RESTRICT', name='fk_schedule_runs_schedule'), + ForeignKeyConstraint(['triggered_by'], ['users.user_id'], ondelete='SET NULL', name='fk_schedule_runs_user'), + ForeignKeyConstraint(['workspace_id'], ['workspaces.workspace_id'], ondelete='RESTRICT', name='fk_schedule_runs_workspace'), + Index('fk_schedule_runs_logs', 'logs_object_id'), + Index('fk_schedule_runs_result', 'result_object_id'), + Index('fk_schedule_runs_user', 'triggered_by'), + Index('idx_schedule_runs_schedule', 'schedule_id', 'created_at'), + Index('idx_schedule_runs_status', 'run_status', 'queued_at'), + Index('idx_schedule_runs_workspace_status', 'workspace_id', 'run_status', 'queued_at'), + Index('uk_schedule_runs_idempotency', 'idempotency_key', unique=True), + {'comment': '调度运行'} + ) + + run_id: Mapped[str] = mapped_column(CHAR(26), primary_key=True) + schedule_id: Mapped[str] = mapped_column(CHAR(26), nullable=False) + workspace_id: Mapped[str] = mapped_column(CHAR(26), nullable=False) + workflow_version: Mapped[int] = mapped_column(INTEGER, nullable=False) + trigger_type: Mapped[str] = mapped_column(String(16), nullable=False, comment='manual/cron/api/retry') + idempotency_key: Mapped[str] = mapped_column(String(128), nullable=False) + run_status: Mapped[str] = mapped_column(String(24), nullable=False, server_default=text("'queued'"), comment='queued/running/succeeded/failed/cancelled/timed_out') + state_version: Mapped[int] = mapped_column(INTEGER, nullable=False, server_default=text("0"), comment='乐观锁版本') + schedule_snapshot: Mapped[dict] = mapped_column(JSON, nullable=False, comment='执行时 DAG 快照') + queued_at: Mapped[datetime.datetime] = mapped_column(DATETIME(fsp=3), nullable=False, server_default=text('CURRENT_TIMESTAMP(3)')) + created_at: Mapped[datetime.datetime] = mapped_column(DATETIME(fsp=3), nullable=False, server_default=text('CURRENT_TIMESTAMP(3)')) + triggered_by: Mapped[Optional[str]] = mapped_column(CHAR(26)) + started_at: Mapped[Optional[datetime.datetime]] = mapped_column(DATETIME(fsp=3)) + finished_at: Mapped[Optional[datetime.datetime]] = mapped_column(DATETIME(fsp=3)) + duration_ms: Mapped[Optional[int]] = mapped_column(BIGINT) + error_code: Mapped[Optional[str]] = mapped_column(String(64)) + error_message: Mapped[Optional[str]] = mapped_column(Text) + logs_object_id: Mapped[Optional[str]] = mapped_column(CHAR(26)) + result_object_id: Mapped[Optional[str]] = mapped_column(CHAR(26)) + + logs_object: Mapped[Optional['StorageObjects']] = relationship('StorageObjects', foreign_keys=[logs_object_id], back_populates='schedule_runs') + result_object: Mapped[Optional['StorageObjects']] = relationship('StorageObjects', foreign_keys=[result_object_id], back_populates='schedule_runs_') + schedule: Mapped['Schedules'] = relationship('Schedules', back_populates='schedule_runs') + users: Mapped[Optional['Users']] = relationship('Users', back_populates='schedule_runs') + workspace: Mapped['Workspaces'] = relationship('Workspaces', back_populates='schedule_runs') + experiments: Mapped[list['Experiments']] = relationship('Experiments', back_populates='schedule_run') + schedule_node_runs: Mapped[list['ScheduleNodeRuns']] = relationship('ScheduleNodeRuns', back_populates='run') + + +class Scripts(Base): + __tablename__ = 'scripts' + __table_args__ = ( + ForeignKeyConstraint(['current_object_id'], ['storage_objects.storage_object_id'], ondelete='RESTRICT', name='fk_scripts_current_object'), + ForeignKeyConstraint(['owner_user_id'], ['users.user_id'], ondelete='RESTRICT', name='fk_scripts_owner'), + ForeignKeyConstraint(['workspace_id'], ['workspaces.workspace_id'], ondelete='RESTRICT', name='fk_scripts_workspace'), + Index('idx_scripts_owner', 'owner_user_id', 'status'), + Index('idx_scripts_workspace', 'workspace_id', 'script_type', 'visibility', 'status'), + Index('uk_scripts_current_object', 'current_object_id', unique=True), + {'comment': '可执行 Python/Notebook 脚本'} + ) + + script_id: Mapped[str] = mapped_column(CHAR(26), primary_key=True) + workspace_id: Mapped[str] = mapped_column(CHAR(26), nullable=False) + current_object_id: Mapped[str] = mapped_column(CHAR(26), nullable=False, comment='当前工作副本') + owner_user_id: Mapped[str] = mapped_column(CHAR(26), nullable=False) + script_name: Mapped[str] = mapped_column(String(255), nullable=False) + script_type: Mapped[str] = mapped_column(String(16), nullable=False, comment='python/notebook') + visibility: Mapped[str] = mapped_column(String(16), nullable=False, server_default=text("'private'")) + status: Mapped[str] = mapped_column(String(16), nullable=False, server_default=text("'active'")) + created_at: Mapped[datetime.datetime] = mapped_column(DATETIME(fsp=3), nullable=False, server_default=text('CURRENT_TIMESTAMP(3)')) + updated_at: Mapped[datetime.datetime] = mapped_column(DATETIME(fsp=3), nullable=False, server_default=text('CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)')) + deleted_at: Mapped[Optional[datetime.datetime]] = mapped_column(DATETIME(fsp=3)) + + current_object: Mapped['StorageObjects'] = relationship('StorageObjects', back_populates='scripts') + owner_user: Mapped['Users'] = relationship('Users', back_populates='scripts') + workspace: Mapped['Workspaces'] = relationship('Workspaces', back_populates='scripts') + notebook_snapshots: Mapped[list['NotebookSnapshots']] = relationship('NotebookSnapshots', back_populates='script') + versions: Mapped[list['Versions']] = relationship('Versions', back_populates='script') + experiments: Mapped[list['Experiments']] = relationship('Experiments', back_populates='script') + + +class UploadSessions(Base): + __tablename__ = 'upload_sessions' + __table_args__ = ( + ForeignKeyConstraint(['storage_object_id'], ['storage_objects.storage_object_id'], ondelete='SET NULL', name='fk_upload_sessions_storage_object'), + ForeignKeyConstraint(['user_id'], ['users.user_id'], ondelete='RESTRICT', name='fk_upload_sessions_user'), + ForeignKeyConstraint(['workspace_id'], ['workspaces.workspace_id'], ondelete='RESTRICT', name='fk_upload_sessions_workspace'), + Index('fk_upload_sessions_storage_object', 'storage_object_id'), + Index('fk_upload_sessions_user', 'user_id'), + Index('idx_upload_sessions_expiry', 'upload_status', 'expires_at'), + Index('idx_upload_sessions_object_key', 'bucket_name', 'object_key_hash'), + Index('idx_upload_sessions_workspace', 'workspace_id', 'user_id', 'created_at'), + Index('uk_upload_sessions_idempotency', 'idempotency_key', unique=True), + {'comment': 'RustFS 预签名上传会话;URL 本身不持久化'} + ) + + upload_id: Mapped[str] = mapped_column(CHAR(26), primary_key=True) + workspace_id: Mapped[str] = mapped_column(CHAR(26), nullable=False) + user_id: Mapped[str] = mapped_column(CHAR(26), nullable=False) + idempotency_key: Mapped[str] = mapped_column(String(128), nullable=False) + bucket_name: Mapped[str] = mapped_column(String(128), nullable=False) + object_key: Mapped[str] = mapped_column(String(1024), nullable=False) + object_key_hash: Mapped[bytes] = mapped_column(BINARY(32), nullable=False) + upload_status: Mapped[str] = mapped_column(String(24), nullable=False, server_default=text("'created'"), comment='created/uploading/completed/expired/aborted/failed') + expires_at: Mapped[datetime.datetime] = mapped_column(DATETIME(fsp=3), nullable=False) + created_at: Mapped[datetime.datetime] = mapped_column(DATETIME(fsp=3), nullable=False, server_default=text('CURRENT_TIMESTAMP(3)')) + updated_at: Mapped[datetime.datetime] = mapped_column(DATETIME(fsp=3), nullable=False, server_default=text('CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)')) + multipart_upload_id: Mapped[Optional[str]] = mapped_column(String(255)) + expected_size_bytes: Mapped[Optional[int]] = mapped_column(BIGINT) + expected_hash: Mapped[Optional[str]] = mapped_column(CHAR(64)) + content_type: Mapped[Optional[str]] = mapped_column(String(255)) + storage_object_id: Mapped[Optional[str]] = mapped_column(CHAR(26)) + completed_at: Mapped[Optional[datetime.datetime]] = mapped_column(DATETIME(fsp=3)) + + storage_object: Mapped[Optional['StorageObjects']] = relationship('StorageObjects', back_populates='upload_sessions') + user: Mapped['Users'] = relationship('Users', back_populates='upload_sessions') + workspace: Mapped['Workspaces'] = relationship('Workspaces', back_populates='upload_sessions') + + +class WorkspaceOperations(Base): + __tablename__ = 'workspace_operations' + __table_args__ = ( + ForeignKeyConstraint(['requested_by'], ['users.user_id'], ondelete='RESTRICT', name='fk_workspace_operations_user'), + ForeignKeyConstraint(['runtime_id'], ['runtime_instances.runtime_id'], ondelete='SET NULL', name='fk_workspace_operations_runtime'), + ForeignKeyConstraint(['workspace_id'], ['workspaces.workspace_id'], ondelete='RESTRICT', name='fk_workspace_operations_workspace'), + Index('fk_workspace_operations_user', 'requested_by'), + Index('idx_workspace_operations_runtime', 'runtime_id', 'created_at'), + Index('idx_workspace_operations_workspace', 'workspace_id', 'operation_status', 'created_at'), + Index('uk_workspace_operations_request', 'request_id', unique=True), + {'comment': '无状态 Backend 的 Workspace/Jupyter 异步操作记录'} + ) + + operation_id: Mapped[str] = mapped_column(CHAR(26), primary_key=True) + workspace_id: Mapped[str] = mapped_column(CHAR(26), nullable=False) + operation_type: Mapped[str] = mapped_column(String(24), nullable=False, comment='open/close/mount/unmount/start/stop/restart/recycle') + operation_status: Mapped[str] = mapped_column(String(24), nullable=False, server_default=text("'pending'"), comment='pending/running/succeeded/failed/cancelled') + state_version: Mapped[int] = mapped_column(INTEGER, nullable=False, server_default=text("0"), comment='乐观锁版本') + requested_by: Mapped[str] = mapped_column(CHAR(26), nullable=False) + created_at: Mapped[datetime.datetime] = mapped_column(DATETIME(fsp=3), nullable=False, server_default=text('CURRENT_TIMESTAMP(3)')) + runtime_id: Mapped[Optional[str]] = mapped_column(CHAR(26)) + request_id: Mapped[Optional[str]] = mapped_column(String(128)) + started_at: Mapped[Optional[datetime.datetime]] = mapped_column(DATETIME(fsp=3)) + finished_at: Mapped[Optional[datetime.datetime]] = mapped_column(DATETIME(fsp=3)) + error_code: Mapped[Optional[str]] = mapped_column(String(64)) + error_message: Mapped[Optional[str]] = mapped_column(Text) + + users: Mapped['Users'] = relationship('Users', back_populates='workspace_operations') + runtime: Mapped[Optional['RuntimeInstances']] = relationship('RuntimeInstances', back_populates='workspace_operations') + workspace: Mapped['Workspaces'] = relationship('Workspaces', back_populates='workspace_operations') + + +class NotebookSnapshots(Base): + __tablename__ = 'notebook_snapshots' + __table_args__ = ( + ForeignKeyConstraint(['artifact_object_id'], ['storage_objects.storage_object_id'], ondelete='RESTRICT', name='fk_snapshots_artifact_object'), + ForeignKeyConstraint(['created_by'], ['users.user_id'], ondelete='RESTRICT', name='fk_snapshots_created_by'), + ForeignKeyConstraint(['script_id'], ['scripts.script_id'], ondelete='RESTRICT', name='fk_snapshots_script'), + ForeignKeyConstraint(['source_object_id'], ['storage_objects.storage_object_id'], ondelete='RESTRICT', name='fk_snapshots_source_object'), + ForeignKeyConstraint(['workspace_id'], ['workspaces.workspace_id'], ondelete='RESTRICT', name='fk_snapshots_workspace'), + Index('fk_snapshots_created_by', 'created_by'), + Index('fk_snapshots_source_object', 'source_object_id'), + Index('idx_snapshots_workspace_created', 'workspace_id', 'created_at'), + Index('uk_snapshots_artifact', 'artifact_object_id', unique=True), + Index('uk_snapshots_script_hash', 'script_id', 'content_hash', unique=True), + {'comment': 'Notebook 开发快照,append-only'} + ) + + snapshot_id: Mapped[str] = mapped_column(CHAR(26), primary_key=True) + workspace_id: Mapped[str] = mapped_column(CHAR(26), nullable=False) + script_id: Mapped[str] = mapped_column(CHAR(26), nullable=False) + source_object_id: Mapped[str] = mapped_column(CHAR(26), nullable=False) + artifact_object_id: Mapped[str] = mapped_column(CHAR(26), nullable=False) + snapshot_name: Mapped[str] = mapped_column(String(255), nullable=False) + content_hash: Mapped[str] = mapped_column(CHAR(64), nullable=False) + outputs_stripped: Mapped[int] = mapped_column(TINYINT(1), nullable=False, server_default=text("1")) + created_by: Mapped[str] = mapped_column(CHAR(26), nullable=False) + created_at: Mapped[datetime.datetime] = mapped_column(DATETIME(fsp=3), nullable=False, server_default=text('CURRENT_TIMESTAMP(3)')) + description: Mapped[Optional[str]] = mapped_column(String(1000)) + + artifact_object: Mapped['StorageObjects'] = relationship('StorageObjects', foreign_keys=[artifact_object_id], back_populates='notebook_snapshots') + users: Mapped['Users'] = relationship('Users', back_populates='notebook_snapshots') + script: Mapped['Scripts'] = relationship('Scripts', back_populates='notebook_snapshots') + source_object: Mapped['StorageObjects'] = relationship('StorageObjects', foreign_keys=[source_object_id], back_populates='notebook_snapshots_') + workspace: Mapped['Workspaces'] = relationship('Workspaces', back_populates='notebook_snapshots') + + +class Versions(Base): + __tablename__ = 'versions' + __table_args__ = ( + ForeignKeyConstraint(['artifact_object_id'], ['storage_objects.storage_object_id'], ondelete='RESTRICT', name='fk_versions_artifact_object'), + ForeignKeyConstraint(['created_by'], ['users.user_id'], ondelete='RESTRICT', name='fk_versions_created_by'), + ForeignKeyConstraint(['script_id'], ['scripts.script_id'], ondelete='RESTRICT', name='fk_versions_script'), + ForeignKeyConstraint(['source_object_id'], ['storage_objects.storage_object_id'], ondelete='RESTRICT', name='fk_versions_source_object'), + ForeignKeyConstraint(['workspace_id'], ['workspaces.workspace_id'], ondelete='RESTRICT', name='fk_versions_workspace'), + Index('fk_versions_source_object', 'source_object_id'), + Index('idx_versions_creator', 'created_by', 'created_at'), + Index('idx_versions_workspace_created', 'workspace_id', 'created_at'), + Index('uk_versions_artifact', 'artifact_object_id', unique=True), + Index('uk_versions_script_hash', 'script_id', 'content_hash', unique=True), + Index('uk_versions_script_no', 'script_id', 'version_no', unique=True), + {'comment': '不可变稳定版本;调度节点必须引用 versions_id'} + ) + + versions_id: Mapped[str] = mapped_column(CHAR(26), primary_key=True, comment='稳定版本唯一 ID') + workspace_id: Mapped[str] = mapped_column(CHAR(26), nullable=False) + script_id: Mapped[str] = mapped_column(CHAR(26), nullable=False) + source_object_id: Mapped[str] = mapped_column(CHAR(26), nullable=False, comment='发布时的源对象') + artifact_object_id: Mapped[str] = mapped_column(CHAR(26), nullable=False, comment='RustFS 不可变版本制品') + version_no: Mapped[int] = mapped_column(INTEGER, nullable=False) + version_label: Mapped[str] = mapped_column(String(32), nullable=False, comment='例如 v1.0') + source_path: Mapped[str] = mapped_column(String(1024), nullable=False, comment='发布时路径快照') + artifact_path: Mapped[str] = mapped_column(String(1500), nullable=False) + content_hash: Mapped[str] = mapped_column(CHAR(64), nullable=False) + file_size_bytes: Mapped[int] = mapped_column(BIGINT, nullable=False, server_default=text("0")) + visibility: Mapped[str] = mapped_column(String(16), nullable=False, server_default=text("'private'")) + created_by: Mapped[str] = mapped_column(CHAR(26), nullable=False) + created_at: Mapped[datetime.datetime] = mapped_column(DATETIME(fsp=3), nullable=False, server_default=text('CURRENT_TIMESTAMP(3)')) + release_note: Mapped[Optional[str]] = mapped_column(String(1000)) + schedule_hidden_at: Mapped[Optional[datetime.datetime]] = mapped_column( + DATETIME(fsp=3), + comment='从调度稳定版本列表移除的时间;不影响版本和运行历史', + ) + + artifact_object: Mapped['StorageObjects'] = relationship('StorageObjects', foreign_keys=[artifact_object_id], back_populates='versions') + users: Mapped['Users'] = relationship('Users', back_populates='versions') + script: Mapped['Scripts'] = relationship('Scripts', back_populates='versions') + source_object: Mapped['StorageObjects'] = relationship('StorageObjects', foreign_keys=[source_object_id], back_populates='versions_') + workspace: Mapped['Workspaces'] = relationship('Workspaces', back_populates='versions') + experiments: Mapped[list['Experiments']] = relationship('Experiments', back_populates='versions') + schedule_nodes: Mapped[list['ScheduleNodes']] = relationship('ScheduleNodes', back_populates='versions') + schedule_node_runs: Mapped[list['ScheduleNodeRuns']] = relationship('ScheduleNodeRuns', back_populates='versions') + + +class Experiments(Base): + __tablename__ = 'experiments' + __table_args__ = ( + ForeignKeyConstraint(['logs_object_id'], ['storage_objects.storage_object_id'], ondelete='SET NULL', name='fk_experiments_logs'), + ForeignKeyConstraint(['owner_user_id'], ['users.user_id'], ondelete='RESTRICT', name='fk_experiments_owner'), + ForeignKeyConstraint(['parent_experiment_id'], ['experiments.experiment_id'], ondelete='SET NULL', name='fk_experiments_parent'), + ForeignKeyConstraint(['result_object_id'], ['storage_objects.storage_object_id'], ondelete='SET NULL', name='fk_experiments_result'), + ForeignKeyConstraint(['schedule_run_id'], ['schedule_runs.run_id'], ondelete='SET NULL', name='fk_experiments_schedule_run'), + ForeignKeyConstraint(['script_id'], ['scripts.script_id'], ondelete='SET NULL', name='fk_experiments_script'), + ForeignKeyConstraint(['versions_id'], ['versions.versions_id'], ondelete='SET NULL', name='fk_experiments_version'), + ForeignKeyConstraint(['workspace_id'], ['workspaces.workspace_id'], ondelete='RESTRICT', name='fk_experiments_workspace'), + Index('fk_experiments_logs', 'logs_object_id'), + Index('fk_experiments_parent', 'parent_experiment_id'), + Index('fk_experiments_result', 'result_object_id'), + Index('fk_experiments_script', 'script_id'), + Index('idx_experiments_owner', 'owner_user_id', 'created_at'), + Index('idx_experiments_schedule_run', 'schedule_run_id'), + Index('idx_experiments_version', 'versions_id'), + Index('idx_experiments_workspace', 'workspace_id', 'experiment_status', 'created_at'), + {'comment': '实验记录'} + ) + + experiment_id: Mapped[str] = mapped_column(CHAR(26), primary_key=True) + workspace_id: Mapped[str] = mapped_column(CHAR(26), nullable=False) + owner_user_id: Mapped[str] = mapped_column(CHAR(26), nullable=False) + experiment_name: Mapped[str] = mapped_column(String(255), nullable=False) + source_type: Mapped[str] = mapped_column(String(24), nullable=False, comment='python/notebook/schedule/rerun') + experiment_status: Mapped[str] = mapped_column(String(24), nullable=False, server_default=text("'queued'")) + state_version: Mapped[int] = mapped_column(INTEGER, nullable=False, server_default=text("0"), comment='乐观锁版本') + created_at: Mapped[datetime.datetime] = mapped_column(DATETIME(fsp=3), nullable=False, server_default=text('CURRENT_TIMESTAMP(3)')) + script_id: Mapped[Optional[str]] = mapped_column(CHAR(26)) + versions_id: Mapped[Optional[str]] = mapped_column(CHAR(26), comment='工作副本运行时可为空') + schedule_run_id: Mapped[Optional[str]] = mapped_column(CHAR(26)) + parent_experiment_id: Mapped[Optional[str]] = mapped_column(CHAR(26)) + parameters_json: Mapped[Optional[dict]] = mapped_column(JSON) + environment_json: Mapped[Optional[dict]] = mapped_column(JSON) + result_summary: Mapped[Optional[str]] = mapped_column(String(2000)) + logs_object_id: Mapped[Optional[str]] = mapped_column(CHAR(26)) + result_object_id: Mapped[Optional[str]] = mapped_column(CHAR(26)) + started_at: Mapped[Optional[datetime.datetime]] = mapped_column(DATETIME(fsp=3)) + finished_at: Mapped[Optional[datetime.datetime]] = mapped_column(DATETIME(fsp=3)) + duration_ms: Mapped[Optional[int]] = mapped_column(BIGINT) + deleted_at: Mapped[Optional[datetime.datetime]] = mapped_column(DATETIME(fsp=3)) + + logs_object: Mapped[Optional['StorageObjects']] = relationship('StorageObjects', foreign_keys=[logs_object_id], back_populates='experiments') + owner_user: Mapped['Users'] = relationship('Users', back_populates='experiments') + parent_experiment: Mapped[Optional['Experiments']] = relationship('Experiments', remote_side=[experiment_id], back_populates='parent_experiment_reverse') + parent_experiment_reverse: Mapped[list['Experiments']] = relationship('Experiments', remote_side=[parent_experiment_id], back_populates='parent_experiment') + result_object: Mapped[Optional['StorageObjects']] = relationship('StorageObjects', foreign_keys=[result_object_id], back_populates='experiments_') + schedule_run: Mapped[Optional['ScheduleRuns']] = relationship('ScheduleRuns', back_populates='experiments') + script: Mapped[Optional['Scripts']] = relationship('Scripts', back_populates='experiments') + versions: Mapped[Optional['Versions']] = relationship('Versions', back_populates='experiments') + workspace: Mapped['Workspaces'] = relationship('Workspaces', back_populates='experiments') + experiment_metrics: Mapped[list['ExperimentMetrics']] = relationship('ExperimentMetrics', back_populates='experiment') + experiment_resources: Mapped[list['ExperimentResources']] = relationship('ExperimentResources', back_populates='experiment') + + +class ScheduleNodes(Base): + __tablename__ = 'schedule_nodes' + __table_args__ = ( + ForeignKeyConstraint(['schedule_id'], ['schedules.schedule_id'], ondelete='CASCADE', name='fk_schedule_nodes_schedule'), + ForeignKeyConstraint(['versions_id'], ['versions.versions_id'], ondelete='RESTRICT', name='fk_schedule_nodes_version'), + Index('idx_schedule_nodes_version', 'versions_id'), + Index('uk_schedule_nodes_key', 'schedule_id', 'node_key', unique=True), + {'comment': 'DAG 节点,必须引用稳定版本'} + ) + + node_id: Mapped[str] = mapped_column(CHAR(26), primary_key=True) + schedule_id: Mapped[str] = mapped_column(CHAR(26), nullable=False) + node_key: Mapped[str] = mapped_column(String(64), nullable=False, comment='画布内稳定标识') + node_name: Mapped[str] = mapped_column(String(255), nullable=False) + versions_id: Mapped[str] = mapped_column(CHAR(26), nullable=False) + timeout_seconds: Mapped[int] = mapped_column(INTEGER, nullable=False, server_default=text("600")) + retry_count: Mapped[int] = mapped_column(INTEGER, nullable=False, server_default=text("0")) + retry_interval_sec: Mapped[int] = mapped_column(INTEGER, nullable=False, server_default=text("5")) + position_x: Mapped[decimal.Decimal] = mapped_column(DECIMAL(10, 2), nullable=False, server_default=text("0.00")) + position_y: Mapped[decimal.Decimal] = mapped_column(DECIMAL(10, 2), nullable=False, server_default=text("0.00")) + created_at: Mapped[datetime.datetime] = mapped_column(DATETIME(fsp=3), nullable=False, server_default=text('CURRENT_TIMESTAMP(3)')) + updated_at: Mapped[datetime.datetime] = mapped_column(DATETIME(fsp=3), nullable=False, server_default=text('CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)')) + arguments_json: Mapped[Optional[dict]] = mapped_column(JSON) + env_refs_json: Mapped[Optional[dict]] = mapped_column(JSON, comment='只存密钥引用,不存明文密钥') + + schedule: Mapped['Schedules'] = relationship('Schedules', back_populates='schedule_nodes') + versions: Mapped['Versions'] = relationship('Versions', back_populates='schedule_nodes') + schedule_edges: Mapped[list['ScheduleEdges']] = relationship('ScheduleEdges', foreign_keys='[ScheduleEdges.source_node_id]', back_populates='source_node') + schedule_edges_: Mapped[list['ScheduleEdges']] = relationship('ScheduleEdges', foreign_keys='[ScheduleEdges.target_node_id]', back_populates='target_node') + schedule_node_runs: Mapped[list['ScheduleNodeRuns']] = relationship('ScheduleNodeRuns', back_populates='node') + + +class ExperimentMetrics(Base): + __tablename__ = 'experiment_metrics' + __table_args__ = ( + ForeignKeyConstraint(['experiment_id'], ['experiments.experiment_id'], ondelete='CASCADE', name='fk_experiment_metrics_experiment'), + Index('idx_experiment_metrics_lookup', 'experiment_id', 'metric_name', 'step_no'), + {'comment': '实验指标,支持筛选和曲线'} + ) + + metric_id: Mapped[int] = mapped_column(BIGINT, primary_key=True) + experiment_id: Mapped[str] = mapped_column(CHAR(26), nullable=False) + metric_name: Mapped[str] = mapped_column(String(128), nullable=False) + recorded_at: Mapped[datetime.datetime] = mapped_column(DATETIME(fsp=3), nullable=False, server_default=text('CURRENT_TIMESTAMP(3)')) + metric_value: Mapped[Optional[decimal.Decimal]] = mapped_column(Double(asdecimal=True)) + metric_text: Mapped[Optional[str]] = mapped_column(String(1000)) + step_no: Mapped[Optional[int]] = mapped_column(BigInteger) + + experiment: Mapped['Experiments'] = relationship('Experiments', back_populates='experiment_metrics') + + +class ExperimentResources(Base): + __tablename__ = 'experiment_resources' + __table_args__ = ( + ForeignKeyConstraint(['experiment_id'], ['experiments.experiment_id'], ondelete='CASCADE', name='fk_experiment_resources_experiment'), + ForeignKeyConstraint(['resource_id'], ['data_resources.resource_id'], ondelete='RESTRICT', name='fk_experiment_resources_resource'), + Index('fk_experiment_resources_resource', 'resource_id'), + {'comment': '实验与数据资源'} + ) + + experiment_id: Mapped[str] = mapped_column(CHAR(26), primary_key=True) + resource_id: Mapped[str] = mapped_column(CHAR(26), primary_key=True) + resource_role: Mapped[str] = mapped_column(String(16), primary_key=True, server_default=text("'input'"), comment='input/output') + created_at: Mapped[datetime.datetime] = mapped_column(DATETIME(fsp=3), nullable=False, server_default=text('CURRENT_TIMESTAMP(3)')) + + experiment: Mapped['Experiments'] = relationship('Experiments', back_populates='experiment_resources') + resource: Mapped['DataResources'] = relationship('DataResources', back_populates='experiment_resources') + + +class ScheduleEdges(Base): + __tablename__ = 'schedule_edges' + __table_args__ = ( + ForeignKeyConstraint(['schedule_id'], ['schedules.schedule_id'], ondelete='CASCADE', name='fk_schedule_edges_schedule'), + ForeignKeyConstraint(['source_node_id'], ['schedule_nodes.node_id'], ondelete='CASCADE', name='fk_schedule_edges_source'), + ForeignKeyConstraint(['target_node_id'], ['schedule_nodes.node_id'], ondelete='CASCADE', name='fk_schedule_edges_target'), + Index('fk_schedule_edges_source', 'source_node_id'), + Index('idx_schedule_edges_target', 'target_node_id'), + Index('uk_schedule_edges_pair', 'schedule_id', 'source_node_id', 'target_node_id', unique=True), + {'comment': 'DAG 有向边'} + ) + + edge_id: Mapped[str] = mapped_column(CHAR(26), primary_key=True) + schedule_id: Mapped[str] = mapped_column(CHAR(26), nullable=False) + source_node_id: Mapped[str] = mapped_column(CHAR(26), nullable=False) + target_node_id: Mapped[str] = mapped_column(CHAR(26), nullable=False) + created_at: Mapped[datetime.datetime] = mapped_column(DATETIME(fsp=3), nullable=False, server_default=text('CURRENT_TIMESTAMP(3)')) + condition_expr: Mapped[Optional[str]] = mapped_column(String(1000)) + + schedule: Mapped['Schedules'] = relationship('Schedules', back_populates='schedule_edges') + source_node: Mapped['ScheduleNodes'] = relationship('ScheduleNodes', foreign_keys=[source_node_id], back_populates='schedule_edges') + target_node: Mapped['ScheduleNodes'] = relationship('ScheduleNodes', foreign_keys=[target_node_id], back_populates='schedule_edges_') + + +class ScheduleNodeRuns(Base): + __tablename__ = 'schedule_node_runs' + __table_args__ = ( + ForeignKeyConstraint(['logs_object_id'], ['storage_objects.storage_object_id'], ondelete='SET NULL', name='fk_node_runs_logs'), + ForeignKeyConstraint(['node_id'], ['schedule_nodes.node_id'], ondelete='RESTRICT', name='fk_node_runs_node'), + ForeignKeyConstraint(['result_object_id'], ['storage_objects.storage_object_id'], ondelete='SET NULL', name='fk_node_runs_result'), + ForeignKeyConstraint(['run_id'], ['schedule_runs.run_id'], ondelete='CASCADE', name='fk_node_runs_run'), + ForeignKeyConstraint(['versions_id'], ['versions.versions_id'], ondelete='RESTRICT', name='fk_node_runs_version'), + Index('fk_node_runs_logs', 'logs_object_id'), + Index('fk_node_runs_node', 'node_id'), + Index('fk_node_runs_result', 'result_object_id'), + Index('idx_node_runs_status', 'run_id', 'node_status'), + Index('idx_node_runs_version', 'versions_id'), + Index('uk_node_runs_attempt', 'run_id', 'node_id', 'attempt_no', unique=True), + {'comment': '调度节点运行与重试'} + ) + + node_run_id: Mapped[str] = mapped_column(CHAR(26), primary_key=True) + run_id: Mapped[str] = mapped_column(CHAR(26), nullable=False) + node_id: Mapped[str] = mapped_column(CHAR(26), nullable=False) + versions_id: Mapped[str] = mapped_column(CHAR(26), nullable=False) + attempt_no: Mapped[int] = mapped_column(INTEGER, nullable=False, server_default=text("1")) + node_status: Mapped[str] = mapped_column(String(24), nullable=False, server_default=text("'queued'"), comment='queued/running/succeeded/failed/skipped/cancelled/timed_out') + state_version: Mapped[int] = mapped_column(INTEGER, nullable=False, server_default=text("0"), comment='乐观锁版本') + created_at: Mapped[datetime.datetime] = mapped_column(DATETIME(fsp=3), nullable=False, server_default=text('CURRENT_TIMESTAMP(3)')) + started_at: Mapped[Optional[datetime.datetime]] = mapped_column(DATETIME(fsp=3)) + finished_at: Mapped[Optional[datetime.datetime]] = mapped_column(DATETIME(fsp=3)) + duration_ms: Mapped[Optional[int]] = mapped_column(BIGINT) + exit_code: Mapped[Optional[int]] = mapped_column(Integer) + message: Mapped[Optional[str]] = mapped_column(String(2000)) + metrics_json: Mapped[Optional[dict]] = mapped_column(JSON) + logs_object_id: Mapped[Optional[str]] = mapped_column(CHAR(26)) + result_object_id: Mapped[Optional[str]] = mapped_column(CHAR(26)) + + logs_object: Mapped[Optional['StorageObjects']] = relationship('StorageObjects', foreign_keys=[logs_object_id], back_populates='schedule_node_runs') + node: Mapped['ScheduleNodes'] = relationship('ScheduleNodes', back_populates='schedule_node_runs') + result_object: Mapped[Optional['StorageObjects']] = relationship('StorageObjects', foreign_keys=[result_object_id], back_populates='schedule_node_runs_') + run: Mapped['ScheduleRuns'] = relationship('ScheduleRuns', back_populates='schedule_node_runs') + versions: Mapped['Versions'] = relationship('Versions', back_populates='schedule_node_runs') diff --git a/common/src/common/db/session.py b/common/src/common/db/session.py index f0ff52a..65ed3ca 100644 --- a/common/src/common/db/session.py +++ b/common/src/common/db/session.py @@ -1,5 +1,51 @@ -# coding=utf-8 -""" -@Time :2026/7/29 -@Author :tao.chen -""" +from __future__ import annotations + +from collections.abc import AsyncIterator +from contextlib import asynccontextmanager + +from sqlalchemy.ext.asyncio import ( + AsyncEngine, + AsyncSession, + async_sessionmaker, + create_async_engine, +) + +AsyncSessionFactory = async_sessionmaker[AsyncSession] + + +def create_database_engine( + database_url: str, + *, + echo: bool = False, + pool_pre_ping: bool = True, +) -> AsyncEngine: + """Create an async SQLAlchemy engine without storing global connection state.""" + return create_async_engine( + database_url, + echo=echo, + pool_pre_ping=pool_pre_ping, + ) + + +def create_session_factory(engine: AsyncEngine) -> AsyncSessionFactory: + """Create the shared async session factory used by FastAPI dependencies.""" + return async_sessionmaker( + bind=engine, + class_=AsyncSession, + expire_on_commit=False, + autoflush=False, + ) + + +@asynccontextmanager +async def session_scope( + factory: AsyncSessionFactory, +) -> AsyncIterator[AsyncSession]: + """Commit a unit of work or roll it back when an exception is raised.""" + async with factory() as session: + try: + yield session + await session.commit() + except Exception: + await session.rollback() + raise diff --git a/common/src/common/eventing.py b/common/src/common/eventing.py new file mode 100644 index 0000000..02f6b6d --- /dev/null +++ b/common/src/common/eventing.py @@ -0,0 +1,71 @@ +from __future__ import annotations + +from datetime import UTC, datetime +from typing import Any + +from sqlalchemy.ext.asyncio import AsyncSession + +from common.db.models import OutboxEvents +from common.ids import new_ulid + + +STREAM_BY_EVENT_TYPE = { + "schedule.run.requested": "stream:scheduler:commands", + "job.node.execute": "stream:jobs:execute", + "job.node.finished": "stream:jobs:results", +} + + +def utcnow() -> datetime: + return datetime.now(UTC).replace(tzinfo=None) + + +def event_time(value: datetime | None = None) -> str: + item = value or utcnow() + if item.tzinfo is None: + item = item.replace(tzinfo=UTC) + return item.astimezone(UTC).isoformat().replace("+00:00", "Z") + + +async def add_outbox_event( + session: AsyncSession, + *, + event_type: str, + producer: str, + trace_id: str, + aggregate_type: str, + aggregate_id: str, + idempotency_key: str, + payload: dict[str, Any], + available_at: datetime | None = None, +) -> OutboxEvents: + if event_type not in STREAM_BY_EVENT_TYPE: + raise ValueError(f"unsupported event type: {event_type}") + event_id = new_ulid() + envelope = { + "event_id": event_id, + "event_type": event_type, + "schema_version": 1, + "occurred_at": event_time(), + "producer": producer, + "trace_id": trace_id, + "aggregate_type": aggregate_type, + "aggregate_id": aggregate_id, + "idempotency_key": idempotency_key, + "payload": payload, + } + item = OutboxEvents( + event_id=event_id, + aggregate_type=aggregate_type, + aggregate_id=aggregate_id, + event_type=event_type, + schema_version=1, + payload_json=envelope, + event_status="pending", + available_at=available_at or utcnow(), + retry_count=0, + trace_id=trace_id, + idempotency_key=idempotency_key, + ) + session.add(item) + return item diff --git a/common/src/common/ids.py b/common/src/common/ids.py new file mode 100644 index 0000000..f738c8a --- /dev/null +++ b/common/src/common/ids.py @@ -0,0 +1,17 @@ +from __future__ import annotations + +import secrets +import time + +_CROCKFORD32 = "0123456789ABCDEFGHJKMNPQRSTVWXYZ" + + +def new_ulid() -> str: + """Return a lexicographically sortable 26-character ULID.""" + timestamp_ms = int(time.time_ns() // 1_000_000) + value = (timestamp_ms << 80) | secrets.randbits(80) + encoded = ["0"] * 26 + for index in range(25, -1, -1): + encoded[index] = _CROCKFORD32[value & 31] + value >>= 5 + return "".join(encoded) diff --git a/common/src/common/service_app.py b/common/src/common/service_app.py new file mode 100644 index 0000000..bebb15c --- /dev/null +++ b/common/src/common/service_app.py @@ -0,0 +1,79 @@ +from __future__ import annotations + +import asyncio +import os +from datetime import UTC, datetime +from typing import Any, Callable + +from fastapi import FastAPI, Response, status + + +def _target_list() -> list[str]: + value = os.getenv("READINESS_TARGETS", "") + return [item.strip() for item in value.split(",") if item.strip()] + + +async def _check_tcp_target(target: str) -> dict[str, Any]: + host, port_text = target.rsplit(":", 1) + try: + reader, writer = await asyncio.wait_for( + asyncio.open_connection(host, int(port_text)), + timeout=1.5, + ) + writer.close() + await writer.wait_closed() + return {"target": target, "status": "ok"} + except (OSError, TimeoutError, ValueError) as exc: + return { + "target": target, + "status": "error", + "detail": type(exc).__name__, + } + + +def create_service_app( + service_name: str, + *, + lifespan: Callable[..., Any] | None = None, +) -> FastAPI: + app = FastAPI( + title=f"{service_name} service", + version="0.1.0", + docs_url="/docs", + redoc_url=None, + lifespan=lifespan, + ) + + def base_payload(state: str) -> dict[str, str]: + return { + "status": state, + "service": service_name, + "timestamp": datetime.now(UTC).isoformat(), + } + + @app.get("/") + async def root() -> dict[str, str]: + return base_payload("running") + + @app.get("/health/live") + async def live() -> dict[str, str]: + return base_payload("ok") + + @app.get("/api/v1/health") + async def public_health() -> dict[str, str]: + return base_payload("ok") + + @app.get("/health/ready") + async def ready(response: Response) -> dict[str, Any]: + checks = await asyncio.gather( + *(_check_tcp_target(target) for target in _target_list()) + ) + ready_state = all(check["status"] == "ok" for check in checks) + if not ready_state: + response.status_code = status.HTTP_503_SERVICE_UNAVAILABLE + return { + **base_payload("ready" if ready_state else "not_ready"), + "checks": checks, + } + + return app diff --git a/common/src/common/storage/__init__.py b/common/src/common/storage/__init__.py index f0ff52a..417f62a 100644 --- a/common/src/common/storage/__init__.py +++ b/common/src/common/storage/__init__.py @@ -1,5 +1,3 @@ -# coding=utf-8 -""" -@Time :2026/7/29 -@Author :tao.chen -""" +from common.storage.rustfs import RustFSObjectStore + +__all__ = ["RustFSObjectStore"] diff --git a/common/src/common/storage/rustfs.py b/common/src/common/storage/rustfs.py index f0ff52a..88bc51c 100644 --- a/common/src/common/storage/rustfs.py +++ b/common/src/common/storage/rustfs.py @@ -1,5 +1,126 @@ -# coding=utf-8 -""" -@Time :2026/7/29 -@Author :tao.chen -""" +from __future__ import annotations + +import hashlib +from typing import Any, BinaryIO + +import boto3 +from botocore.client import Config +from botocore.exceptions import ClientError + + +class RustFSObjectStore: + def __init__( + self, + *, + internal_endpoint: str, + public_endpoint: str, + access_key: str, + secret_key: str, + ) -> None: + common = { + "aws_access_key_id": access_key, + "aws_secret_access_key": secret_key, + "region_name": "us-east-1", + "config": Config( + signature_version="s3v4", + s3={"addressing_style": "path"}, + ), + } + self.internal = boto3.client( + "s3", + endpoint_url=internal_endpoint.rstrip("/"), + **common, + ) + self.public = boto3.client( + "s3", + endpoint_url=public_endpoint.rstrip("/"), + **common, + ) + + def ensure_bucket(self, bucket_name: str) -> None: + try: + self.internal.head_bucket(Bucket=bucket_name) + except ClientError as exc: + code = str(exc.response.get("Error", {}).get("Code", "")) + if code not in {"404", "NoSuchBucket", "NotFound"}: + raise + self.internal.create_bucket(Bucket=bucket_name) + + def presign_put( + self, + *, + bucket_name: str, + object_key: str, + content_type: str, + expected_hash: str | None, + expires_seconds: int, + public: bool, + ) -> tuple[str, dict[str, str]]: + params: dict[str, Any] = { + "Bucket": bucket_name, + "Key": object_key, + "ContentType": content_type, + } + headers = {"Content-Type": content_type} + if expected_hash: + params["Metadata"] = {"sha256": expected_hash} + headers["x-amz-meta-sha256"] = expected_hash + client = self.public if public else self.internal + url = client.generate_presigned_url( + "put_object", + Params=params, + ExpiresIn=expires_seconds, + ) + return url, headers + + def presign_get( + self, + *, + bucket_name: str, + object_key: str, + file_name: str, + expires_seconds: int, + ) -> str: + return self.public.generate_presigned_url( + "get_object", + Params={ + "Bucket": bucket_name, + "Key": object_key, + "ResponseContentDisposition": ( + f'attachment; filename="{file_name.encode("ascii", "ignore").decode() or "download"}"' + ), + }, + ExpiresIn=expires_seconds, + ) + + def put_bytes( + self, + *, + bucket_name: str, + object_key: str, + content: bytes, + content_type: str, + content_hash: str, + ) -> None: + self.internal.put_object( + Bucket=bucket_name, + Key=object_key, + Body=content, + ContentType=content_type, + Metadata={"sha256": content_hash}, + ) + + def head(self, *, bucket_name: str, object_key: str) -> dict[str, Any]: + return self.internal.head_object(Bucket=bucket_name, Key=object_key) + + def sha256(self, *, bucket_name: str, object_key: str) -> str: + response = self.internal.get_object(Bucket=bucket_name, Key=object_key) + body: BinaryIO = response["Body"] + digest = hashlib.sha256() + while chunk := body.read(1024 * 1024): + digest.update(chunk) + body.close() + return digest.hexdigest() + + def delete(self, *, bucket_name: str, object_key: str) -> None: + self.internal.delete_object(Bucket=bucket_name, Key=object_key) diff --git a/contracts/README.md b/contracts/README.md new file mode 100644 index 0000000..5c1a927 --- /dev/null +++ b/contracts/README.md @@ -0,0 +1,15 @@ +# Contracts + +模块接口契约公共目录: + +```text +openapi/ HTTP OpenAPI 3 契约 +events/ Redis Streams JSON Schema +runtime/ Runtime Adapter 契约 +locks/ Notebook 编辑锁契约 +``` + +各服务实现必须以本目录中的版本化契约为准。 + +快速 Demo 的跨模块边界、状态枚举和事件路由统一见 +`demo-core-v1.md`。 diff --git a/contracts/__init__.py b/contracts/__init__.py new file mode 100644 index 0000000..3992080 --- /dev/null +++ b/contracts/__init__.py @@ -0,0 +1 @@ +"""Executable cross-service contracts.""" diff --git a/contracts/demo-core-v1.md b/contracts/demo-core-v1.md new file mode 100644 index 0000000..b30696b --- /dev/null +++ b/contracts/demo-core-v1.md @@ -0,0 +1,57 @@ +# 快速 Demo 核心契约 V1 + +冻结日期:2026-07-27 +状态:`frozen-target` + +## 1. 契约边界 + +Demo 公共 HTTP API 由两部分共同组成: + +1. `openapi/platform-api-v1.yaml`:已经运行的脚本、稳定版本详情、数据资源和文件编辑锁接口。 +2. `openapi/demo-core-extension-v1.yaml`:第 15 小步冻结、后续小步实现的上下文、Jupyter 票据、稳定版本列表、调度和运行查询接口。 + +内部 Storage 与 Runtime 接口继续分别以 +`openapi/storage-api-internal-v1.yaml` 和 +`openapi/runtime-api-internal-v1.yaml` 为准。 + +## 2. 不可变约束 + +- 主标识使用 ULID;调度节点只引用不可变的 `versions_id`。 +- 当前认证上下文由 `X-User-ID`、`X-Workspace-ID` 提供;以后换成 JWT 时不得改变业务 DTO。 +- 修改调度方案必须提交 `workflow_version`,冲突返回 `412`。 +- 创建调度、立即运行、启停和取消使用 `Idempotency-Key`。 +- `schedule_runs.schedule_snapshot` 固化本次执行 DAG,后续编辑不影响已创建的运行。 +- 浏览器不能拿到 Jupyter 内部 Token,只能获得短期访问 Cookie。 + +## 3. 状态值 + +- 调度运行:`queued / running / succeeded / failed / cancelled / timed_out` +- 节点运行:`queued / running / succeeded / failed / skipped / cancelled / timed_out` +- 失败策略:`stop / continue` +- 触发类型:`manual / cron / api / retry` + +## 4. Redis Streams 路由 + +| Stream | Consumer Group | 事件 | 生产者 | 消费者 | +|---|---|---|---|---| +| `stream:scheduler:commands` | `schedule-orchestrator` | `schedule.run.requested` | Platform API / Cron Dispatcher | Schedule Orchestrator | +| `stream:jobs:execute` | `job-workers` | `job.node.execute` | Schedule Orchestrator | Job Worker | +| `stream:jobs:results` | `schedule-results` | `job.node.finished` | Job Worker | Schedule Orchestrator | + +交付语义为至少一次。业务事务先写 `outbox_events`,发布成功后更新 +Outbox;消费者处理前以 `consumer_inbox` 去重,业务更新与 Inbox 写入同一 +MySQL 事务。只有业务事务提交成功后才确认 Redis 消息。 + +## 5. 模块所有权 + +| 模块 | 拥有的数据与职责 | +|---|---| +| Platform API | 脚本、稳定版本、调度定义、运行查询、Outbox 写入 | +| Runtime Manager | Workspace/Jupyter 生命周期与内部运行态 | +| Nginx | 统一入口、Jupyter HTTP/WebSocket 代理 | +| Schedule Orchestrator | 消费调度请求、解析 DAG、推进节点状态 | +| Job Worker | 读取稳定版本、执行节点、保存日志/结果、报告终态 | +| Storage Service | RustFS 对象元数据与预签名访问 | + +模块间不得复制定义 DTO、状态枚举、错误结构或事件字段;契约变更必须先 +升级本目录中的版本化文件。 diff --git a/contracts/events/README.md b/contracts/events/README.md new file mode 100644 index 0000000..38b8415 --- /dev/null +++ b/contracts/events/README.md @@ -0,0 +1,11 @@ +# Event Schemas + +Redis Streams 事件以 JSON Schema Draft 2020-12 定义: + +- `event-envelope-v1.json`:公共事件信封。 +- `schedule-run-requested-v1.json`:请求启动一次调度运行。 +- `job-node-execute-v1.json`:请求 Worker 执行一个稳定版本节点。 +- `job-node-finished-v1.json`:Worker 报告节点终态。 + +所有事件使用至少一次投递、Transactional Outbox 与 Consumer Inbox。 +字段和状态值不得在生产者或消费者中另行定义。 diff --git a/contracts/events/event-envelope-v1.json b/contracts/events/event-envelope-v1.json new file mode 100644 index 0000000..feb5136 --- /dev/null +++ b/contracts/events/event-envelope-v1.json @@ -0,0 +1,72 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "event-envelope-v1.json", + "title": "Model Platform Event Envelope V1", + "description": "Redis Streams 中所有业务事件共用的不可变信封。", + "type": "object", + "additionalProperties": false, + "required": [ + "event_id", + "event_type", + "schema_version", + "occurred_at", + "producer", + "trace_id", + "aggregate_type", + "aggregate_id", + "idempotency_key", + "payload" + ], + "properties": { + "event_id": { + "$ref": "#/$defs/ulid" + }, + "event_type": { + "type": "string", + "pattern": "^[a-z][a-z0-9]*(\\.[a-z][a-z0-9_]*)+$" + }, + "schema_version": { + "const": 1 + }, + "occurred_at": { + "type": "string", + "format": "date-time" + }, + "producer": { + "type": "string", + "minLength": 1, + "maxLength": 64 + }, + "trace_id": { + "type": "string", + "minLength": 1, + "maxLength": 64 + }, + "aggregate_type": { + "type": "string", + "minLength": 1, + "maxLength": 64 + }, + "aggregate_id": { + "type": "string", + "minLength": 1, + "maxLength": 128 + }, + "idempotency_key": { + "type": "string", + "minLength": 8, + "maxLength": 128 + }, + "payload": { + "type": "object" + } + }, + "$defs": { + "ulid": { + "type": "string", + "minLength": 26, + "maxLength": 26, + "pattern": "^[0-9A-HJKMNP-TV-Z]{26}$" + } + } +} diff --git a/contracts/events/job-node-execute-v1.json b/contracts/events/job-node-execute-v1.json new file mode 100644 index 0000000..423ad25 --- /dev/null +++ b/contracts/events/job-node-execute-v1.json @@ -0,0 +1,125 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "job-node-execute-v1.json", + "title": "Job Node Execute V1", + "description": "调度编排器请求 Job Worker 执行一个稳定版本节点。", + "allOf": [ + { + "$ref": "event-envelope-v1.json" + }, + { + "type": "object", + "properties": { + "event_type": { + "const": "job.node.execute" + }, + "aggregate_type": { + "const": "schedule_node_run" + }, + "payload": { + "$ref": "job-node-execute-v1.json#/$defs/payload" + } + } + } + ], + "$defs": { + "ulid": { + "type": "string", + "minLength": 26, + "maxLength": 26, + "pattern": "^[0-9A-HJKMNP-TV-Z]{26}$" + }, + "payload": { + "type": "object", + "additionalProperties": false, + "required": [ + "workspace_id", + "run_id", + "node_run_id", + "node_id", + "versions_id", + "attempt_no", + "script_type", + "artifact_object_id", + "artifact_path", + "timeout_seconds", + "arguments" + ], + "properties": { + "workspace_id": { + "$ref": "job-node-execute-v1.json#/$defs/ulid" + }, + "run_id": { + "$ref": "job-node-execute-v1.json#/$defs/ulid" + }, + "node_run_id": { + "$ref": "job-node-execute-v1.json#/$defs/ulid" + }, + "node_id": { + "$ref": "job-node-execute-v1.json#/$defs/ulid" + }, + "versions_id": { + "$ref": "job-node-execute-v1.json#/$defs/ulid" + }, + "attempt_no": { + "type": "integer", + "minimum": 1, + "maximum": 11 + }, + "script_type": { + "enum": [ + "python", + "notebook" + ] + }, + "artifact_object_id": { + "$ref": "job-node-execute-v1.json#/$defs/ulid" + }, + "artifact_path": { + "type": "string", + "minLength": 1, + "maxLength": 1024 + }, + "timeout_seconds": { + "type": "integer", + "minimum": 1, + "maximum": 86400 + }, + "arguments": { + "type": "array", + "maxItems": 100, + "items": { + "type": "string", + "maxLength": 1000 + } + } + } + } + }, + "examples": [ + { + "event_id": "01K123456789ABCDEFGHJKMNPZ", + "event_type": "job.node.execute", + "schema_version": 1, + "occurred_at": "2026-07-27T08:00:01Z", + "producer": "schedule-orchestrator", + "trace_id": "req-demo-0001", + "aggregate_type": "schedule_node_run", + "aggregate_id": "01K123456789ABCDEFGHJKMNQ0", + "idempotency_key": "01K123456789ABCDEFGHJKMNQ0:1", + "payload": { + "workspace_id": "01K123456789ABCDEFGHJKMNPS", + "run_id": "01K123456789ABCDEFGHJKMNPR", + "node_run_id": "01K123456789ABCDEFGHJKMNQ0", + "node_id": "01K123456789ABCDEFGHJKMNPW", + "versions_id": "01K123456789ABCDEFGHJKMNPX", + "attempt_no": 1, + "script_type": "python", + "artifact_object_id": "01K123456789ABCDEFGHJKMNPY", + "artifact_path": "versions/prepare/v1.py", + "timeout_seconds": 600, + "arguments": [] + } + } + ] +} diff --git a/contracts/events/job-node-finished-v1.json b/contracts/events/job-node-finished-v1.json new file mode 100644 index 0000000..4a925cc --- /dev/null +++ b/contracts/events/job-node-finished-v1.json @@ -0,0 +1,162 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "job-node-finished-v1.json", + "title": "Job Node Finished V1", + "description": "Job Worker 报告一次节点执行的终态,调度编排器据此推进 DAG。", + "allOf": [ + { + "$ref": "event-envelope-v1.json" + }, + { + "type": "object", + "properties": { + "event_type": { + "const": "job.node.finished" + }, + "aggregate_type": { + "const": "schedule_node_run" + }, + "payload": { + "$ref": "job-node-finished-v1.json#/$defs/payload" + } + } + } + ], + "$defs": { + "ulid": { + "type": "string", + "minLength": 26, + "maxLength": 26, + "pattern": "^[0-9A-HJKMNP-TV-Z]{26}$" + }, + "nullableUlid": { + "oneOf": [ + { + "$ref": "job-node-finished-v1.json#/$defs/ulid" + }, + { + "type": "null" + } + ] + }, + "payload": { + "type": "object", + "additionalProperties": false, + "required": [ + "workspace_id", + "run_id", + "node_run_id", + "node_id", + "versions_id", + "attempt_no", + "node_status", + "exit_code", + "started_at", + "finished_at", + "duration_ms", + "logs_object_id", + "result_object_id", + "error_code", + "error_message" + ], + "properties": { + "workspace_id": { + "$ref": "job-node-finished-v1.json#/$defs/ulid" + }, + "run_id": { + "$ref": "job-node-finished-v1.json#/$defs/ulid" + }, + "node_run_id": { + "$ref": "job-node-finished-v1.json#/$defs/ulid" + }, + "node_id": { + "$ref": "job-node-finished-v1.json#/$defs/ulid" + }, + "versions_id": { + "$ref": "job-node-finished-v1.json#/$defs/ulid" + }, + "attempt_no": { + "type": "integer", + "minimum": 1, + "maximum": 11 + }, + "node_status": { + "enum": [ + "succeeded", + "failed", + "cancelled", + "timed_out" + ] + }, + "exit_code": { + "type": [ + "integer", + "null" + ] + }, + "started_at": { + "type": "string", + "format": "date-time" + }, + "finished_at": { + "type": "string", + "format": "date-time" + }, + "duration_ms": { + "type": "integer", + "minimum": 0 + }, + "logs_object_id": { + "$ref": "job-node-finished-v1.json#/$defs/nullableUlid" + }, + "result_object_id": { + "$ref": "job-node-finished-v1.json#/$defs/nullableUlid" + }, + "error_code": { + "type": [ + "string", + "null" + ], + "maxLength": 64 + }, + "error_message": { + "type": [ + "string", + "null" + ], + "maxLength": 2000 + } + } + } + }, + "examples": [ + { + "event_id": "01K123456789ABCDEFGHJKMNQ1", + "event_type": "job.node.finished", + "schema_version": 1, + "occurred_at": "2026-07-27T08:00:03Z", + "producer": "job-worker", + "trace_id": "req-demo-0001", + "aggregate_type": "schedule_node_run", + "aggregate_id": "01K123456789ABCDEFGHJKMNQ0", + "idempotency_key": "01K123456789ABCDEFGHJKMNQ0:1:finished", + "payload": { + "workspace_id": "01K123456789ABCDEFGHJKMNPS", + "run_id": "01K123456789ABCDEFGHJKMNPR", + "node_run_id": "01K123456789ABCDEFGHJKMNQ0", + "node_id": "01K123456789ABCDEFGHJKMNPW", + "versions_id": "01K123456789ABCDEFGHJKMNPX", + "attempt_no": 1, + "node_status": "succeeded", + "exit_code": 0, + "started_at": "2026-07-27T08:00:01Z", + "finished_at": "2026-07-27T08:00:03Z", + "duration_ms": 2000, + "logs_object_id": null, + "result_object_id": null, + "error_code": null, + "error_message": null + } + } + ] +} diff --git a/contracts/events/schedule-run-requested-v1.json b/contracts/events/schedule-run-requested-v1.json new file mode 100644 index 0000000..e8603e3 --- /dev/null +++ b/contracts/events/schedule-run-requested-v1.json @@ -0,0 +1,255 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schedule-run-requested-v1.json", + "title": "Schedule Run Requested V1", + "description": "Platform API 或 Cron Dispatcher 请求调度编排器启动一次固化 DAG。", + "allOf": [ + { + "$ref": "event-envelope-v1.json" + }, + { + "type": "object", + "properties": { + "event_type": { + "const": "schedule.run.requested" + }, + "aggregate_type": { + "const": "schedule_run" + }, + "payload": { + "$ref": "schedule-run-requested-v1.json#/$defs/payload" + } + } + } + ], + "$defs": { + "ulid": { + "type": "string", + "minLength": 26, + "maxLength": 26, + "pattern": "^[0-9A-HJKMNP-TV-Z]{26}$" + }, + "node": { + "type": "object", + "additionalProperties": false, + "required": [ + "node_id", + "node_key", + "versions_id", + "script_type", + "artifact_object_id", + "artifact_path", + "timeout_seconds", + "retry_count", + "retry_interval_sec", + "arguments" + ], + "properties": { + "node_id": { + "$ref": "schedule-run-requested-v1.json#/$defs/ulid" + }, + "node_key": { + "type": "string", + "minLength": 1, + "maxLength": 64 + }, + "versions_id": { + "$ref": "schedule-run-requested-v1.json#/$defs/ulid" + }, + "script_type": { + "enum": [ + "python", + "notebook" + ] + }, + "artifact_object_id": { + "$ref": "schedule-run-requested-v1.json#/$defs/ulid" + }, + "artifact_path": { + "type": "string", + "minLength": 1, + "maxLength": 1024 + }, + "timeout_seconds": { + "type": "integer", + "minimum": 1, + "maximum": 86400 + }, + "retry_count": { + "type": "integer", + "minimum": 0, + "maximum": 10 + }, + "retry_interval_sec": { + "type": "integer", + "minimum": 0, + "maximum": 3600 + }, + "arguments": { + "type": "array", + "maxItems": 100, + "items": { + "type": "string", + "maxLength": 1000 + } + } + } + }, + "edge": { + "type": "object", + "additionalProperties": false, + "required": [ + "source_node_id", + "target_node_id" + ], + "properties": { + "source_node_id": { + "$ref": "schedule-run-requested-v1.json#/$defs/ulid" + }, + "target_node_id": { + "$ref": "schedule-run-requested-v1.json#/$defs/ulid" + } + } + }, + "snapshot": { + "type": "object", + "additionalProperties": false, + "required": [ + "schedule_name", + "workflow_version", + "max_concurrency", + "failure_policy", + "nodes", + "edges" + ], + "properties": { + "schedule_name": { + "type": "string", + "minLength": 1, + "maxLength": 255 + }, + "workflow_version": { + "type": "integer", + "minimum": 1 + }, + "max_concurrency": { + "type": "integer", + "minimum": 1, + "maximum": 100 + }, + "failure_policy": { + "enum": [ + "stop", + "continue" + ] + }, + "nodes": { + "type": "array", + "minItems": 1, + "maxItems": 100, + "items": { + "$ref": "schedule-run-requested-v1.json#/$defs/node" + } + }, + "edges": { + "type": "array", + "maxItems": 500, + "items": { + "$ref": "schedule-run-requested-v1.json#/$defs/edge" + } + } + } + }, + "payload": { + "type": "object", + "additionalProperties": false, + "required": [ + "workspace_id", + "schedule_id", + "run_id", + "workflow_version", + "trigger_type", + "triggered_by", + "schedule_snapshot" + ], + "properties": { + "workspace_id": { + "$ref": "schedule-run-requested-v1.json#/$defs/ulid" + }, + "schedule_id": { + "$ref": "schedule-run-requested-v1.json#/$defs/ulid" + }, + "run_id": { + "$ref": "schedule-run-requested-v1.json#/$defs/ulid" + }, + "workflow_version": { + "type": "integer", + "minimum": 1 + }, + "trigger_type": { + "enum": [ + "manual", + "cron", + "api", + "retry" + ] + }, + "triggered_by": { + "oneOf": [ + { + "$ref": "schedule-run-requested-v1.json#/$defs/ulid" + }, + { + "type": "null" + } + ] + }, + "schedule_snapshot": { + "$ref": "schedule-run-requested-v1.json#/$defs/snapshot" + } + } + } + }, + "examples": [ + { + "event_id": "01K123456789ABCDEFGHJKMNPQ", + "event_type": "schedule.run.requested", + "schema_version": 1, + "occurred_at": "2026-07-27T08:00:00Z", + "producer": "platform-api", + "trace_id": "req-demo-0001", + "aggregate_type": "schedule_run", + "aggregate_id": "01K123456789ABCDEFGHJKMNPR", + "idempotency_key": "run-demo-0001", + "payload": { + "workspace_id": "01K123456789ABCDEFGHJKMNPS", + "schedule_id": "01K123456789ABCDEFGHJKMNPT", + "run_id": "01K123456789ABCDEFGHJKMNPR", + "workflow_version": 1, + "trigger_type": "manual", + "triggered_by": "01K123456789ABCDEFGHJKMNPV", + "schedule_snapshot": { + "schedule_name": "每日模型演示", + "workflow_version": 1, + "max_concurrency": 1, + "failure_policy": "stop", + "nodes": [ + { + "node_id": "01K123456789ABCDEFGHJKMNPW", + "node_key": "prepare", + "versions_id": "01K123456789ABCDEFGHJKMNPX", + "script_type": "python", + "artifact_object_id": "01K123456789ABCDEFGHJKMNPY", + "artifact_path": "versions/prepare/v1.py", + "timeout_seconds": 600, + "retry_count": 0, + "retry_interval_sec": 5, + "arguments": [] + } + ], + "edges": [] + } + } + } + ] +} diff --git a/contracts/locks/README.md b/contracts/locks/README.md new file mode 100644 index 0000000..c8a8f12 --- /dev/null +++ b/contracts/locks/README.md @@ -0,0 +1,4 @@ +# 编辑锁契约 + +当前版本见 [file-edit-lock-v1.md](file-edit-lock-v1.md)。Redis 是实时锁唯一 +权威,MySQL `edit_sessions` 仅用于审计。 diff --git a/contracts/locks/file-edit-lock-v1.md b/contracts/locks/file-edit-lock-v1.md new file mode 100644 index 0000000..af49c36 --- /dev/null +++ b/contracts/locks/file-edit-lock-v1.md @@ -0,0 +1,63 @@ +# 文件编辑锁契约 v1 + +## 公共接口 + +```text +POST /api/v1/files/{storage_object_id}/lock +POST /api/v1/file-locks/{edit_session_id}/heartbeat +DELETE /api/v1/file-locks/{edit_session_id} +``` + +三个接口均要求用户身份和 Workspace 身份。当前开发基线使用 +`X-User-ID`、`X-Workspace-ID`,后续接入 JWT 时保持路径和业务 DTO 不变。 + +加锁成功返回一次性原始 `lock_token`。心跳和释放请求体均为: + +```json +{"lock_token": "raw-token-returned-by-acquire"} +``` + +原始 token 只由客户端持有,禁止写入 MySQL 和日志。 + +## Redis 数据 + +```text +Key: lock:file:{workspace_id}:{storage_object_id} +TTL: 45000 ms +``` + +Value: + +```json +{ + "edit_session_id": "01J...", + "user_id": "01J...", + "display_name": "张三", + "token_hash": "sha256-hex", + "acquired_at": "UTC timestamp" +} +``` + +加锁必须使用: + +```text +SET key value NX PX 45000 +``` + +心跳和释放必须执行 Lua 原子操作,并同时比较 +`edit_session_id + token_hash`: + +```text +heartbeat: compare owner -> PEXPIRE 45000 +release: compare owner -> DEL +``` + +禁止使用 `GET` 后单独 `DEL`,也禁止在 MySQL 文件表增加 `is_locked`。 + +## 状态与错误 + +- Redis 是实时锁唯一权威。 +- `edit_sessions` 记录 `active/closed/expired`,仅用于审计。 +- 冲突返回 HTTP 409、错误码 `FILE_LOCK_CONFLICT`,并包含当前编辑者及租约到期时间。 +- 错误 token 返回 HTTP 403,且不得续期或释放现有锁。 +- 浏览器建议每 15 秒心跳;关闭、断线或心跳超时后由主动释放或 TTL 释放。 diff --git a/contracts/openapi/README.md b/contracts/openapi/README.md new file mode 100644 index 0000000..cfc99f3 --- /dev/null +++ b/contracts/openapi/README.md @@ -0,0 +1,11 @@ +# OpenAPI 契约 + +- `platform-api-v1.yaml`:浏览器访问的公共业务接口。 +- `demo-core-extension-v1.yaml`:第 15 小步冻结的 Demo 目标扩展接口; + 与 `platform-api-v1.yaml` 合并后构成完整公共 API v1。 +- `storage-api-internal-v1.yaml`:Storage Service 内部接口。 +- `runtime-api-internal-v1.yaml`:Runtime Manager 内部接口。 + +现有服务契约由对应 FastAPI 应用生成;目标扩展契约先冻结、后实现,不能 +标记为已上线。所有文件均纳入结构校验。公共接口统一使用 `/api/v1`, +内部服务接口统一使用 `/internal/v1`。 diff --git a/contracts/openapi/demo-core-extension-v1.yaml b/contracts/openapi/demo-core-extension-v1.yaml new file mode 100644 index 0000000..d81930e --- /dev/null +++ b/contracts/openapi/demo-core-extension-v1.yaml @@ -0,0 +1,1093 @@ +openapi: 3.1.0 +info: + title: Model Platform Demo Core Extension + version: 1.0.0 + description: | + 第 15 小步冻结的快速 Demo 目标契约。此文件只描述尚待实现的公共接口; + 已实现的脚本、稳定版本详情、文件锁和数据资源接口继续以 + platform-api-v1.yaml 为准。两个文件共同组成 Demo 公共 API v1。 + 当前身份仍使用 X-User-ID 与 X-Workspace-ID,后续替换 JWT 时不改变业务 DTO。 +x-implementation-status: frozen-target +paths: + /api/v1/session-context: + get: + tags: [context] + operationId: getSessionContext + summary: 获取当前用户、Workspace 和可切换上下文 + parameters: + - $ref: '#/components/parameters/XUserId' + - $ref: '#/components/parameters/XWorkspaceId' + - $ref: '#/components/parameters/XRequestId' + responses: + '200': + description: 当前演示上下文 + content: + application/json: + schema: + $ref: '#/components/schemas/SessionContextResponse' + '403': + $ref: '#/components/responses/ErrorResponse' + + /api/v1/jupyter/access-tickets: + post: + tags: [jupyter] + operationId: createJupyterAccessTicket + x-implementation-status: implemented-step16 + summary: 为已加锁编辑会话签发短期 Jupyter 访问票据 + description: | + 必须校验 edit_session_id、用户、Workspace 和 lock_token。 + 成功响应同时设置 HttpOnly、SameSite=Lax、Path=/jupyter/ 的 + jupyter_access Cookie,浏览器不得获得 Jupyter 内部服务 token。 + parameters: + - $ref: '#/components/parameters/XUserId' + - $ref: '#/components/parameters/XWorkspaceId' + - $ref: '#/components/parameters/XRequestId' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateJupyterAccessTicketRequest' + responses: + '201': + description: 访问票据已签发 + headers: + Set-Cookie: + schema: + type: string + description: jupyter_access HttpOnly Cookie + content: + application/json: + schema: + $ref: '#/components/schemas/JupyterAccessTicketResponse' + '403': + $ref: '#/components/responses/ErrorResponse' + '409': + $ref: '#/components/responses/ErrorResponse' + + /api/v1/versions: + get: + tags: [versions] + operationId: listStableVersions + summary: 获取调度画布可用的稳定版本 + parameters: + - $ref: '#/components/parameters/XUserId' + - $ref: '#/components/parameters/XWorkspaceId' + - $ref: '#/components/parameters/XRequestId' + - name: keyword + in: query + required: false + schema: + type: string + maxLength: 100 + - name: script_type + in: query + required: false + schema: + type: string + enum: [python, notebook] + - name: limit + in: query + required: false + schema: + type: integer + minimum: 1 + maximum: 200 + default: 100 + responses: + '200': + description: 当前 Workspace 可见稳定版本 + content: + application/json: + schema: + $ref: '#/components/schemas/StableVersionListResponse' + '403': + $ref: '#/components/responses/ErrorResponse' + + /api/v1/schedules: + get: + tags: [schedules] + operationId: listSchedules + summary: 查询调度方案 + parameters: + - $ref: '#/components/parameters/XUserId' + - $ref: '#/components/parameters/XWorkspaceId' + - $ref: '#/components/parameters/XRequestId' + - name: keyword + in: query + required: false + schema: + type: string + maxLength: 100 + - name: enabled + in: query + required: false + schema: + type: boolean + responses: + '200': + description: 调度方案列表 + content: + application/json: + schema: + $ref: '#/components/schemas/ScheduleListResponse' + '403': + $ref: '#/components/responses/ErrorResponse' + post: + tags: [schedules] + operationId: createSchedule + summary: 创建调度方案及完整 DAG + parameters: + - $ref: '#/components/parameters/XUserId' + - $ref: '#/components/parameters/XWorkspaceId' + - $ref: '#/components/parameters/XRequestId' + - $ref: '#/components/parameters/IdempotencyKey' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateScheduleRequest' + responses: + '201': + description: 调度方案已创建 + content: + application/json: + schema: + $ref: '#/components/schemas/ScheduleResponse' + '409': + $ref: '#/components/responses/ErrorResponse' + '422': + $ref: '#/components/responses/ErrorResponse' + + /api/v1/schedules/preview: + post: + tags: [schedules] + operationId: previewScheduleCron + summary: 校验 Cron 并预览未来执行时间 + parameters: + - $ref: '#/components/parameters/XUserId' + - $ref: '#/components/parameters/XWorkspaceId' + - $ref: '#/components/parameters/XRequestId' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CronPreviewRequest' + responses: + '200': + description: Cron 有效 + content: + application/json: + schema: + $ref: '#/components/schemas/CronPreviewResponse' + '422': + $ref: '#/components/responses/ErrorResponse' + + /api/v1/schedules/{schedule_id}: + parameters: + - $ref: '#/components/parameters/ScheduleId' + get: + tags: [schedules] + operationId: getSchedule + summary: 获取调度方案及完整 DAG + parameters: + - $ref: '#/components/parameters/XUserId' + - $ref: '#/components/parameters/XWorkspaceId' + - $ref: '#/components/parameters/XRequestId' + responses: + '200': + description: 调度方案详情 + content: + application/json: + schema: + $ref: '#/components/schemas/ScheduleResponse' + '404': + $ref: '#/components/responses/ErrorResponse' + put: + tags: [schedules] + operationId: updateSchedule + summary: 使用 workflow_version 乐观锁替换调度方案和 DAG + parameters: + - $ref: '#/components/parameters/XUserId' + - $ref: '#/components/parameters/XWorkspaceId' + - $ref: '#/components/parameters/XRequestId' + - $ref: '#/components/parameters/IdempotencyKey' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateScheduleRequest' + responses: + '200': + description: 调度方案已更新,workflow_version 已递增 + content: + application/json: + schema: + $ref: '#/components/schemas/ScheduleResponse' + '404': + $ref: '#/components/responses/ErrorResponse' + '409': + $ref: '#/components/responses/ErrorResponse' + '412': + $ref: '#/components/responses/ErrorResponse' + '422': + $ref: '#/components/responses/ErrorResponse' + delete: + tags: [schedules] + operationId: deleteSchedule + summary: 软删除调度方案并保留历史运行 + parameters: + - $ref: '#/components/parameters/XUserId' + - $ref: '#/components/parameters/XWorkspaceId' + - $ref: '#/components/parameters/XRequestId' + responses: + '200': + description: 调度方案已删除 + content: + application/json: + schema: + $ref: '#/components/schemas/ScheduleResponse' + '404': + $ref: '#/components/responses/ErrorResponse' + + /api/v1/schedules/{schedule_id}/run: + post: + tags: [schedule-runs] + operationId: runScheduleNow + summary: 为当前 workflow_version 创建一次立即运行 + parameters: + - $ref: '#/components/parameters/ScheduleId' + - $ref: '#/components/parameters/XUserId' + - $ref: '#/components/parameters/XWorkspaceId' + - $ref: '#/components/parameters/XRequestId' + - $ref: '#/components/parameters/IdempotencyKey' + requestBody: + required: false + content: + application/json: + schema: + $ref: '#/components/schemas/RunScheduleRequest' + responses: + '202': + description: 运行事件已进入 Transactional Outbox + content: + application/json: + schema: + $ref: '#/components/schemas/ScheduleRunResponse' + '404': + $ref: '#/components/responses/ErrorResponse' + '409': + $ref: '#/components/responses/ErrorResponse' + + /api/v1/schedules/{schedule_id}/enable: + post: + tags: [schedules] + operationId: enableSchedule + summary: 启用 Cron 调度 + parameters: + - $ref: '#/components/parameters/ScheduleId' + - $ref: '#/components/parameters/XUserId' + - $ref: '#/components/parameters/XWorkspaceId' + - $ref: '#/components/parameters/XRequestId' + - $ref: '#/components/parameters/IdempotencyKey' + responses: + '200': + description: 调度已启用 + content: + application/json: + schema: + $ref: '#/components/schemas/ScheduleResponse' + '404': + $ref: '#/components/responses/ErrorResponse' + '409': + $ref: '#/components/responses/ErrorResponse' + + /api/v1/schedules/{schedule_id}/disable: + post: + tags: [schedules] + operationId: disableSchedule + summary: 暂停 Cron 调度 + parameters: + - $ref: '#/components/parameters/ScheduleId' + - $ref: '#/components/parameters/XUserId' + - $ref: '#/components/parameters/XWorkspaceId' + - $ref: '#/components/parameters/XRequestId' + - $ref: '#/components/parameters/IdempotencyKey' + responses: + '200': + description: 调度已暂停 + content: + application/json: + schema: + $ref: '#/components/schemas/ScheduleResponse' + '404': + $ref: '#/components/responses/ErrorResponse' + + /api/v1/schedule-runs: + get: + tags: [schedule-runs] + operationId: listScheduleRuns + summary: 查询最近调度运行 + parameters: + - $ref: '#/components/parameters/XUserId' + - $ref: '#/components/parameters/XWorkspaceId' + - $ref: '#/components/parameters/XRequestId' + - name: schedule_id + in: query + required: false + schema: + $ref: '#/components/schemas/Ulid' + - name: status + in: query + required: false + schema: + $ref: '#/components/schemas/RunStatus' + - name: limit + in: query + required: false + schema: + type: integer + minimum: 1 + maximum: 200 + default: 50 + responses: + '200': + description: 最近运行列表 + content: + application/json: + schema: + $ref: '#/components/schemas/ScheduleRunListResponse' + '403': + $ref: '#/components/responses/ErrorResponse' + + /api/v1/schedule-runs/{run_id}: + get: + tags: [schedule-runs] + operationId: getScheduleRun + summary: 获取运行及各节点状态 + parameters: + - $ref: '#/components/parameters/RunId' + - $ref: '#/components/parameters/XUserId' + - $ref: '#/components/parameters/XWorkspaceId' + - $ref: '#/components/parameters/XRequestId' + responses: + '200': + description: 调度运行详情 + content: + application/json: + schema: + $ref: '#/components/schemas/ScheduleRunResponse' + '404': + $ref: '#/components/responses/ErrorResponse' + + /api/v1/schedule-runs/{run_id}/cancel: + post: + tags: [schedule-runs] + operationId: cancelScheduleRun + summary: 请求取消尚未结束的调度运行 + parameters: + - $ref: '#/components/parameters/RunId' + - $ref: '#/components/parameters/XUserId' + - $ref: '#/components/parameters/XWorkspaceId' + - $ref: '#/components/parameters/XRequestId' + - $ref: '#/components/parameters/IdempotencyKey' + responses: + '202': + description: 取消请求已接受 + content: + application/json: + schema: + $ref: '#/components/schemas/ScheduleRunResponse' + '404': + $ref: '#/components/responses/ErrorResponse' + '409': + $ref: '#/components/responses/ErrorResponse' + +components: + parameters: + XUserId: + name: X-User-ID + in: header + required: true + schema: + $ref: '#/components/schemas/Ulid' + XWorkspaceId: + name: X-Workspace-ID + in: header + required: true + schema: + $ref: '#/components/schemas/Ulid' + XRequestId: + name: X-Request-ID + in: header + required: false + schema: + type: string + minLength: 1 + maxLength: 64 + IdempotencyKey: + name: Idempotency-Key + in: header + required: true + schema: + type: string + minLength: 8 + maxLength: 128 + ScheduleId: + name: schedule_id + in: path + required: true + schema: + $ref: '#/components/schemas/Ulid' + RunId: + name: run_id + in: path + required: true + schema: + $ref: '#/components/schemas/Ulid' + + responses: + ErrorResponse: + description: 标准错误响应 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorEnvelope' + + schemas: + Ulid: + type: string + minLength: 26 + maxLength: 26 + pattern: '^[0-9A-HJKMNP-TV-Z]{26}$' + + UtcDateTime: + type: string + format: date-time + + RunStatus: + type: string + enum: [queued, running, succeeded, failed, cancelled, timed_out] + + NodeRunStatus: + type: string + enum: [queued, running, succeeded, failed, skipped, cancelled, timed_out] + + ErrorEnvelope: + type: object + additionalProperties: false + required: [request_id, error] + properties: + request_id: + type: string + error: + type: object + additionalProperties: false + required: [code, message, retryable, details] + properties: + code: + type: string + pattern: '^[A-Z][A-Z0-9_]+$' + message: + type: string + retryable: + type: boolean + details: + type: object + + UserSummary: + type: object + additionalProperties: false + required: [user_id, username, display_name, role_code] + properties: + user_id: + $ref: '#/components/schemas/Ulid' + username: + type: string + display_name: + type: string + role_code: + type: string + + WorkspaceSummary: + type: object + additionalProperties: false + required: [workspace_id, workspace_code, workspace_name] + properties: + workspace_id: + $ref: '#/components/schemas/Ulid' + workspace_code: + type: string + workspace_name: + type: string + + SessionContextResponse: + type: object + additionalProperties: false + required: [request_id, data, meta] + properties: + request_id: + type: string + data: + type: object + additionalProperties: false + required: [current_user, current_workspace, workspaces, workspace_members] + properties: + current_user: + $ref: '#/components/schemas/UserSummary' + current_workspace: + $ref: '#/components/schemas/WorkspaceSummary' + workspaces: + type: array + items: + $ref: '#/components/schemas/WorkspaceSummary' + workspace_members: + type: array + items: + $ref: '#/components/schemas/UserSummary' + meta: + type: object + + CreateJupyterAccessTicketRequest: + type: object + additionalProperties: false + required: [edit_session_id, lock_token] + properties: + edit_session_id: + $ref: '#/components/schemas/Ulid' + lock_token: + type: string + minLength: 32 + maxLength: 256 + + JupyterAccessTicketResponse: + type: object + additionalProperties: false + required: [request_id, data, meta] + properties: + request_id: + type: string + data: + type: object + additionalProperties: false + required: [edit_session_id, jupyter_url, expires_at] + properties: + edit_session_id: + $ref: '#/components/schemas/Ulid' + jupyter_url: + type: string + pattern: '^/jupyter/' + expires_at: + $ref: '#/components/schemas/UtcDateTime' + meta: + type: object + + StableVersionSummary: + type: object + additionalProperties: false + required: + - versions_id + - script_id + - script_name + - script_type + - version_label + - artifact_object_id + - source_path + - visibility + - created_by + - created_at + properties: + versions_id: + $ref: '#/components/schemas/Ulid' + script_id: + $ref: '#/components/schemas/Ulid' + script_name: + type: string + script_type: + type: string + enum: [python, notebook] + version_label: + type: string + artifact_object_id: + $ref: '#/components/schemas/Ulid' + source_path: + type: string + visibility: + type: string + enum: [private, workspace, public] + created_by: + $ref: '#/components/schemas/Ulid' + created_at: + $ref: '#/components/schemas/UtcDateTime' + + StableVersionListResponse: + type: object + additionalProperties: false + required: [request_id, data, meta] + properties: + request_id: + type: string + data: + type: array + items: + $ref: '#/components/schemas/StableVersionSummary' + meta: + type: object + required: [count] + properties: + count: + type: integer + minimum: 0 + + ScheduleNodeInput: + type: object + required: + - node_key + - node_name + - versions_id + - timeout_seconds + - retry_count + - retry_interval_sec + - position_x + - position_y + - arguments + properties: + node_key: + type: string + minLength: 1 + maxLength: 64 + pattern: '^[A-Za-z0-9_-]+$' + node_name: + type: string + minLength: 1 + maxLength: 255 + versions_id: + $ref: '#/components/schemas/Ulid' + timeout_seconds: + type: integer + minimum: 1 + maximum: 86400 + default: 600 + retry_count: + type: integer + minimum: 0 + maximum: 10 + default: 0 + retry_interval_sec: + type: integer + minimum: 0 + maximum: 3600 + default: 5 + position_x: + type: number + position_y: + type: number + arguments: + type: array + maxItems: 100 + items: + type: string + maxLength: 1000 + + ScheduleEdgeInput: + type: object + required: [source_node_key, target_node_key] + properties: + source_node_key: + type: string + minLength: 1 + maxLength: 64 + target_node_key: + type: string + minLength: 1 + maxLength: 64 + + ScheduleDefinitionBase: + type: object + required: + - schedule_name + - trigger_type + - timezone + - max_concurrency + - failure_policy + - nodes + - edges + properties: + schedule_name: + type: string + minLength: 1 + maxLength: 255 + description: + type: [string, 'null'] + maxLength: 1000 + trigger_type: + type: string + enum: [manual, cron, api] + default: cron + cron_expression: + type: [string, 'null'] + maxLength: 128 + timezone: + type: string + minLength: 1 + maxLength: 64 + default: Asia/Shanghai + max_concurrency: + type: integer + minimum: 1 + maximum: 100 + default: 1 + failure_policy: + type: string + enum: [stop, continue] + default: stop + nodes: + type: array + minItems: 1 + maxItems: 100 + items: + $ref: '#/components/schemas/ScheduleNodeInput' + edges: + type: array + maxItems: 500 + items: + $ref: '#/components/schemas/ScheduleEdgeInput' + + CreateScheduleRequest: + unevaluatedProperties: false + allOf: + - $ref: '#/components/schemas/ScheduleDefinitionBase' + + UpdateScheduleRequest: + unevaluatedProperties: false + allOf: + - $ref: '#/components/schemas/ScheduleDefinitionBase' + - type: object + required: [workflow_version] + properties: + workflow_version: + type: integer + minimum: 1 + + ScheduleNode: + unevaluatedProperties: false + allOf: + - $ref: '#/components/schemas/ScheduleNodeInput' + - type: object + required: [node_id] + properties: + node_id: + $ref: '#/components/schemas/Ulid' + + ScheduleEdge: + unevaluatedProperties: false + allOf: + - $ref: '#/components/schemas/ScheduleEdgeInput' + - type: object + required: [edge_id, source_node_id, target_node_id] + properties: + edge_id: + $ref: '#/components/schemas/Ulid' + source_node_id: + $ref: '#/components/schemas/Ulid' + target_node_id: + $ref: '#/components/schemas/Ulid' + + Schedule: + type: object + additionalProperties: false + required: + - schedule_id + - workspace_id + - schedule_name + - trigger_type + - timezone + - enabled + - workflow_version + - max_concurrency + - failure_policy + - nodes + - edges + - created_by + - updated_by + - created_at + - updated_at + properties: + schedule_id: + $ref: '#/components/schemas/Ulid' + workspace_id: + $ref: '#/components/schemas/Ulid' + schedule_name: + type: string + description: + type: [string, 'null'] + trigger_type: + type: string + enum: [manual, cron, api] + cron_expression: + type: [string, 'null'] + timezone: + type: string + enabled: + type: boolean + workflow_version: + type: integer + minimum: 1 + max_concurrency: + type: integer + failure_policy: + type: string + enum: [stop, continue] + next_run_at: + type: [string, 'null'] + format: date-time + last_run_at: + type: [string, 'null'] + format: date-time + nodes: + type: array + items: + $ref: '#/components/schemas/ScheduleNode' + edges: + type: array + items: + $ref: '#/components/schemas/ScheduleEdge' + created_by: + $ref: '#/components/schemas/Ulid' + updated_by: + $ref: '#/components/schemas/Ulid' + created_at: + $ref: '#/components/schemas/UtcDateTime' + updated_at: + $ref: '#/components/schemas/UtcDateTime' + + ScheduleResponse: + type: object + additionalProperties: false + required: [request_id, data, meta] + properties: + request_id: + type: string + data: + $ref: '#/components/schemas/Schedule' + meta: + type: object + + ScheduleListResponse: + type: object + additionalProperties: false + required: [request_id, data, meta] + properties: + request_id: + type: string + data: + type: array + items: + $ref: '#/components/schemas/Schedule' + meta: + type: object + required: [count] + properties: + count: + type: integer + minimum: 0 + + CronPreviewRequest: + type: object + additionalProperties: false + required: [cron_expression, timezone] + properties: + cron_expression: + type: string + minLength: 9 + maxLength: 128 + timezone: + type: string + minLength: 1 + maxLength: 64 + default: Asia/Shanghai + count: + type: integer + minimum: 1 + maximum: 20 + default: 5 + + CronPreviewResponse: + type: object + additionalProperties: false + required: [request_id, data, meta] + properties: + request_id: + type: string + data: + type: object + additionalProperties: false + required: [cron_expression, timezone, next_runs] + properties: + cron_expression: + type: string + timezone: + type: string + next_runs: + type: array + items: + $ref: '#/components/schemas/UtcDateTime' + meta: + type: object + + RunScheduleRequest: + type: object + additionalProperties: false + properties: + reason: + type: string + maxLength: 255 + default: manual_run + + ScheduleRunSummary: + type: object + required: + - run_id + - schedule_id + - workspace_id + - workflow_version + - trigger_type + - run_status + - state_version + - queued_at + properties: + run_id: + $ref: '#/components/schemas/Ulid' + schedule_id: + $ref: '#/components/schemas/Ulid' + workspace_id: + $ref: '#/components/schemas/Ulid' + workflow_version: + type: integer + trigger_type: + type: string + enum: [manual, cron, api, retry] + run_status: + $ref: '#/components/schemas/RunStatus' + state_version: + type: integer + minimum: 0 + queued_at: + $ref: '#/components/schemas/UtcDateTime' + started_at: + type: [string, 'null'] + format: date-time + finished_at: + type: [string, 'null'] + format: date-time + duration_ms: + type: [integer, 'null'] + minimum: 0 + error_code: + type: [string, 'null'] + error_message: + type: [string, 'null'] + logs_object_id: + oneOf: + - $ref: '#/components/schemas/Ulid' + - type: 'null' + result_object_id: + oneOf: + - $ref: '#/components/schemas/Ulid' + - type: 'null' + + ScheduleNodeRun: + type: object + additionalProperties: false + required: + - node_run_id + - run_id + - node_id + - versions_id + - attempt_no + - node_status + - state_version + properties: + node_run_id: + $ref: '#/components/schemas/Ulid' + run_id: + $ref: '#/components/schemas/Ulid' + node_id: + $ref: '#/components/schemas/Ulid' + versions_id: + $ref: '#/components/schemas/Ulid' + attempt_no: + type: integer + minimum: 1 + node_status: + $ref: '#/components/schemas/NodeRunStatus' + state_version: + type: integer + minimum: 0 + started_at: + type: [string, 'null'] + format: date-time + finished_at: + type: [string, 'null'] + format: date-time + duration_ms: + type: [integer, 'null'] + minimum: 0 + exit_code: + type: [integer, 'null'] + message: + type: [string, 'null'] + logs_object_id: + oneOf: + - $ref: '#/components/schemas/Ulid' + - type: 'null' + result_object_id: + oneOf: + - $ref: '#/components/schemas/Ulid' + - type: 'null' + + ScheduleRunDetail: + unevaluatedProperties: false + allOf: + - $ref: '#/components/schemas/ScheduleRunSummary' + - type: object + required: [node_runs] + properties: + node_runs: + type: array + items: + $ref: '#/components/schemas/ScheduleNodeRun' + + ScheduleRunResponse: + type: object + additionalProperties: false + required: [request_id, data, meta] + properties: + request_id: + type: string + data: + $ref: '#/components/schemas/ScheduleRunDetail' + meta: + type: object + + ScheduleRunListResponse: + type: object + additionalProperties: false + required: [request_id, data, meta] + properties: + request_id: + type: string + data: + type: array + items: + $ref: '#/components/schemas/ScheduleRunSummary' + meta: + type: object + required: [count] + properties: + count: + type: integer + minimum: 0 diff --git a/contracts/openapi/platform-api-v1.yaml b/contracts/openapi/platform-api-v1.yaml new file mode 100644 index 0000000..3cb378d --- /dev/null +++ b/contracts/openapi/platform-api-v1.yaml @@ -0,0 +1,3080 @@ +openapi: 3.1.0 +info: + title: platform-api service + version: 0.1.0 +paths: + /: + get: + summary: Root + operationId: root__get + responses: + '200': + description: Successful Response + content: + application/json: + schema: + additionalProperties: + type: string + type: object + title: Response Root Get + /health/live: + get: + summary: Live + operationId: live_health_live_get + responses: + '200': + description: Successful Response + content: + application/json: + schema: + additionalProperties: + type: string + type: object + title: Response Live Health Live Get + /api/v1/health: + get: + summary: Public Health + operationId: public_health_api_v1_health_get + responses: + '200': + description: Successful Response + content: + application/json: + schema: + additionalProperties: + type: string + type: object + title: Response Public Health Api V1 Health Get + /health/ready: + get: + summary: Ready + operationId: ready_health_ready_get + responses: + '200': + description: Successful Response + content: + application/json: + schema: + additionalProperties: true + type: object + title: Response Ready Health Ready Get + /api/v1/files/{storage_object_id}/lock: + post: + tags: + - file-locks + summary: Acquire File Lock + operationId: acquire_file_lock_api_v1_files__storage_object_id__lock_post + parameters: + - name: storage_object_id + in: path + required: true + schema: + type: string + title: Storage Object Id + - name: X-User-ID + in: header + required: true + schema: + type: string + title: X-User-Id + - name: X-Workspace-ID + in: header + required: true + schema: + type: string + title: X-Workspace-Id + - name: X-Request-ID + in: header + required: false + schema: + anyOf: + - type: string + - type: 'null' + title: X-Request-Id + responses: + '201': + description: Successful Response + content: + application/json: + schema: {} + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + /api/v1/file-locks/{edit_session_id}/heartbeat: + post: + tags: + - file-locks + summary: Heartbeat File Lock + operationId: heartbeat_file_lock_api_v1_file_locks__edit_session_id__heartbeat_post + parameters: + - name: edit_session_id + in: path + required: true + schema: + type: string + title: Edit Session Id + - name: X-User-ID + in: header + required: true + schema: + type: string + title: X-User-Id + - name: X-Workspace-ID + in: header + required: true + schema: + type: string + title: X-Workspace-Id + - name: X-Request-ID + in: header + required: false + schema: + anyOf: + - type: string + - type: 'null' + title: X-Request-Id + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/FileLockTokenRequest' + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + /api/v1/file-locks/{edit_session_id}: + delete: + tags: + - file-locks + summary: Release File Lock + operationId: release_file_lock_api_v1_file_locks__edit_session_id__delete + parameters: + - name: edit_session_id + in: path + required: true + schema: + type: string + title: Edit Session Id + - name: X-User-ID + in: header + required: true + schema: + type: string + title: X-User-Id + - name: X-Workspace-ID + in: header + required: true + schema: + type: string + title: X-Workspace-Id + - name: X-Request-ID + in: header + required: false + schema: + anyOf: + - type: string + - type: 'null' + title: X-Request-Id + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/FileLockTokenRequest' + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + /api/v1/jupyter/access-tickets: + post: + tags: + - jupyter + summary: Create Jupyter Access Ticket + operationId: create_jupyter_access_ticket_api_v1_jupyter_access_tickets_post + parameters: + - name: X-User-ID + in: header + required: true + schema: + type: string + title: X-User-Id + - name: X-Workspace-ID + in: header + required: true + schema: + type: string + title: X-Workspace-Id + - name: X-Request-ID + in: header + required: false + schema: + anyOf: + - type: string + - type: 'null' + title: X-Request-Id + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateJupyterAccessTicketRequest' + responses: + '201': + description: Successful Response + content: + application/json: + schema: {} + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + /api/v1/data-resources/uploads: + post: + tags: + - data-resources + summary: Create Resource Upload + operationId: create_resource_upload_api_v1_data_resources_uploads_post + parameters: + - name: Idempotency-Key + in: header + required: true + schema: + type: string + minLength: 8 + maxLength: 128 + title: Idempotency-Key + - name: X-User-ID + in: header + required: true + schema: + type: string + title: X-User-Id + - name: X-Workspace-ID + in: header + required: true + schema: + type: string + title: X-Workspace-Id + - name: X-Request-ID + in: header + required: false + schema: + anyOf: + - type: string + - type: 'null' + title: X-Request-Id + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateResourceUploadRequest' + responses: + '201': + description: Successful Response + content: + application/json: + schema: + type: object + additionalProperties: true + title: Response Create Resource Upload Api V1 Data Resources Uploads Post + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + /api/v1/data-resources/uploads/{upload_id}/complete: + post: + tags: + - data-resources + summary: Complete Resource Upload + operationId: complete_resource_upload_api_v1_data_resources_uploads__upload_id__complete_post + parameters: + - name: upload_id + in: path + required: true + schema: + type: string + title: Upload Id + - name: X-User-ID + in: header + required: true + schema: + type: string + title: X-User-Id + - name: X-Workspace-ID + in: header + required: true + schema: + type: string + title: X-Workspace-Id + - name: X-Request-ID + in: header + required: false + schema: + anyOf: + - type: string + - type: 'null' + title: X-Request-Id + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CompleteResourceUploadRequest' + responses: + '200': + description: Successful Response + content: + application/json: + schema: + type: object + additionalProperties: true + title: Response Complete Resource Upload Api V1 Data Resources Uploads Upload Id Complete + Post + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + /api/v1/data-resources: + get: + tags: + - data-resources + summary: List Resources + operationId: list_resources_api_v1_data_resources_get + parameters: + - name: visibility + in: query + required: false + schema: + anyOf: + - type: string + - type: 'null' + title: Visibility + - name: keyword + in: query + required: false + schema: + anyOf: + - type: string + maxLength: 100 + - type: 'null' + title: Keyword + - name: X-User-ID + in: header + required: true + schema: + type: string + title: X-User-Id + - name: X-Workspace-ID + in: header + required: true + schema: + type: string + title: X-Workspace-Id + - name: X-Request-ID + in: header + required: false + schema: + anyOf: + - type: string + - type: 'null' + title: X-Request-Id + responses: + '200': + description: Successful Response + content: + application/json: + schema: + type: object + additionalProperties: true + title: Response List Resources Api V1 Data Resources Get + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + /api/v1/data-resources/{resource_id}: + get: + tags: + - data-resources + summary: Get Resource + operationId: get_resource_api_v1_data_resources__resource_id__get + parameters: + - name: resource_id + in: path + required: true + schema: + type: string + title: Resource Id + - name: X-User-ID + in: header + required: true + schema: + type: string + title: X-User-Id + - name: X-Workspace-ID + in: header + required: true + schema: + type: string + title: X-Workspace-Id + - name: X-Request-ID + in: header + required: false + schema: + anyOf: + - type: string + - type: 'null' + title: X-Request-Id + responses: + '200': + description: Successful Response + content: + application/json: + schema: + type: object + additionalProperties: true + title: Response Get Resource Api V1 Data Resources Resource Id Get + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + delete: + tags: + - data-resources + summary: Delete Resource + operationId: delete_resource_api_v1_data_resources__resource_id__delete + parameters: + - name: resource_id + in: path + required: true + schema: + type: string + title: Resource Id + - name: X-User-ID + in: header + required: true + schema: + type: string + title: X-User-Id + - name: X-Workspace-ID + in: header + required: true + schema: + type: string + title: X-Workspace-Id + - name: X-Request-ID + in: header + required: false + schema: + anyOf: + - type: string + - type: 'null' + title: X-Request-Id + responses: + '200': + description: Successful Response + content: + application/json: + schema: + type: object + additionalProperties: true + title: Response Delete Resource Api V1 Data Resources Resource Id Delete + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + /api/v1/data-resources/{resource_id}/download-url: + post: + tags: + - data-resources + summary: Resource Download Url + operationId: resource_download_url_api_v1_data_resources__resource_id__download_url_post + parameters: + - name: resource_id + in: path + required: true + schema: + type: string + title: Resource Id + - name: X-User-ID + in: header + required: true + schema: + type: string + title: X-User-Id + - name: X-Workspace-ID + in: header + required: true + schema: + type: string + title: X-Workspace-Id + - name: X-Request-ID + in: header + required: false + schema: + anyOf: + - type: string + - type: 'null' + title: X-Request-Id + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/DownloadUrlRequest' + responses: + '200': + description: Successful Response + content: + application/json: + schema: + type: object + additionalProperties: true + title: Response Resource Download Url Api V1 Data Resources Resource Id Download Url + Post + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + /api/v1/schedules/{schedule_id}/run: + post: + tags: + - schedule-runs + summary: Run Schedule Now + operationId: run_schedule_now_api_v1_schedules__schedule_id__run_post + parameters: + - name: schedule_id + in: path + required: true + schema: + type: string + title: Schedule Id + - name: Idempotency-Key + in: header + required: true + schema: + type: string + title: Idempotency-Key + - name: X-User-ID + in: header + required: true + schema: + type: string + title: X-User-Id + - name: X-Workspace-ID + in: header + required: true + schema: + type: string + title: X-Workspace-Id + - name: X-Request-ID + in: header + required: false + schema: + anyOf: + - type: string + - type: 'null' + title: X-Request-Id + requestBody: + content: + application/json: + schema: + anyOf: + - $ref: '#/components/schemas/RunScheduleRequest' + - type: 'null' + title: Payload + responses: + '202': + description: Successful Response + content: + application/json: + schema: + type: object + additionalProperties: true + title: Response Run Schedule Now Api V1 Schedules Schedule Id Run Post + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + /api/v1/schedule-runs: + get: + tags: + - schedule-runs + summary: List Schedule Runs + operationId: list_schedule_runs_api_v1_schedule_runs_get + parameters: + - name: schedule_id + in: query + required: false + schema: + anyOf: + - type: string + - type: 'null' + title: Schedule Id + - name: status + in: query + required: false + schema: + anyOf: + - enum: + - queued + - running + - succeeded + - failed + - cancelled + - timed_out + type: string + - type: 'null' + title: Status + - name: limit + in: query + required: false + schema: + type: integer + maximum: 200 + minimum: 1 + default: 50 + title: Limit + - name: X-User-ID + in: header + required: true + schema: + type: string + title: X-User-Id + - name: X-Workspace-ID + in: header + required: true + schema: + type: string + title: X-Workspace-Id + - name: X-Request-ID + in: header + required: false + schema: + anyOf: + - type: string + - type: 'null' + title: X-Request-Id + responses: + '200': + description: Successful Response + content: + application/json: + schema: + type: object + additionalProperties: true + title: Response List Schedule Runs Api V1 Schedule Runs Get + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + /api/v1/schedule-runs/{run_id}: + get: + tags: + - schedule-runs + summary: Get Schedule Run + operationId: get_schedule_run_api_v1_schedule_runs__run_id__get + parameters: + - name: run_id + in: path + required: true + schema: + type: string + title: Run Id + - name: X-User-ID + in: header + required: true + schema: + type: string + title: X-User-Id + - name: X-Workspace-ID + in: header + required: true + schema: + type: string + title: X-Workspace-Id + - name: X-Request-ID + in: header + required: false + schema: + anyOf: + - type: string + - type: 'null' + title: X-Request-Id + responses: + '200': + description: Successful Response + content: + application/json: + schema: + type: object + additionalProperties: true + title: Response Get Schedule Run Api V1 Schedule Runs Run Id Get + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + /api/v1/cron/preview: + post: + tags: + - schedules + summary: Preview Cron + operationId: preview_cron_api_v1_cron_preview_post + parameters: + - name: X-User-ID + in: header + required: true + schema: + type: string + title: X-User-Id + - name: X-Workspace-ID + in: header + required: true + schema: + type: string + title: X-Workspace-Id + - name: X-Request-ID + in: header + required: false + schema: + anyOf: + - type: string + - type: 'null' + title: X-Request-Id + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CronPreviewRequest' + responses: + '200': + description: Successful Response + content: + application/json: + schema: + type: object + additionalProperties: true + title: Response Preview Cron Api V1 Cron Preview Post + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + /api/v1/schedule-artifacts: + get: + tags: + - schedules + summary: List Schedule Artifacts + operationId: list_schedule_artifacts_api_v1_schedule_artifacts_get + parameters: + - name: limit + in: query + required: false + schema: + type: integer + maximum: 500 + minimum: 1 + default: 100 + title: Limit + - name: X-User-ID + in: header + required: true + schema: + type: string + title: X-User-Id + - name: X-Workspace-ID + in: header + required: true + schema: + type: string + title: X-Workspace-Id + - name: X-Request-ID + in: header + required: false + schema: + anyOf: + - type: string + - type: 'null' + title: X-Request-Id + responses: + '200': + description: Successful Response + content: + application/json: + schema: + type: object + additionalProperties: true + title: Response List Schedule Artifacts Api V1 Schedule Artifacts Get + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + /api/v1/schedules: + get: + tags: + - schedules + summary: List Schedules + operationId: list_schedules_api_v1_schedules_get + parameters: + - name: X-User-ID + in: header + required: true + schema: + type: string + title: X-User-Id + - name: X-Workspace-ID + in: header + required: true + schema: + type: string + title: X-Workspace-Id + - name: X-Request-ID + in: header + required: false + schema: + anyOf: + - type: string + - type: 'null' + title: X-Request-Id + responses: + '200': + description: Successful Response + content: + application/json: + schema: + type: object + additionalProperties: true + title: Response List Schedules Api V1 Schedules Get + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + post: + tags: + - schedules + summary: Create Schedule + operationId: create_schedule_api_v1_schedules_post + parameters: + - name: X-User-ID + in: header + required: true + schema: + type: string + title: X-User-Id + - name: X-Workspace-ID + in: header + required: true + schema: + type: string + title: X-Workspace-Id + - name: X-Request-ID + in: header + required: false + schema: + anyOf: + - type: string + - type: 'null' + title: X-Request-Id + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateScheduleRequest' + responses: + '201': + description: Successful Response + content: + application/json: + schema: + type: object + additionalProperties: true + title: Response Create Schedule Api V1 Schedules Post + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + /api/v1/schedules/{schedule_id}: + get: + tags: + - schedules + summary: Get Schedule + operationId: get_schedule_api_v1_schedules__schedule_id__get + parameters: + - name: schedule_id + in: path + required: true + schema: + type: string + title: Schedule Id + - name: X-User-ID + in: header + required: true + schema: + type: string + title: X-User-Id + - name: X-Workspace-ID + in: header + required: true + schema: + type: string + title: X-Workspace-Id + - name: X-Request-ID + in: header + required: false + schema: + anyOf: + - type: string + - type: 'null' + title: X-Request-Id + responses: + '200': + description: Successful Response + content: + application/json: + schema: + type: object + additionalProperties: true + title: Response Get Schedule Api V1 Schedules Schedule Id Get + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + patch: + tags: + - schedules + summary: Update Schedule + operationId: update_schedule_api_v1_schedules__schedule_id__patch + parameters: + - name: schedule_id + in: path + required: true + schema: + type: string + title: Schedule Id + - name: X-User-ID + in: header + required: true + schema: + type: string + title: X-User-Id + - name: X-Workspace-ID + in: header + required: true + schema: + type: string + title: X-Workspace-Id + - name: X-Request-ID + in: header + required: false + schema: + anyOf: + - type: string + - type: 'null' + title: X-Request-Id + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateScheduleRequest' + responses: + '200': + description: Successful Response + content: + application/json: + schema: + type: object + additionalProperties: true + title: Response Update Schedule Api V1 Schedules Schedule Id Patch + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + put: + tags: + - schedules + summary: Update Schedule + operationId: update_schedule_api_v1_schedules__schedule_id__put + parameters: + - name: schedule_id + in: path + required: true + schema: + type: string + title: Schedule Id + - name: X-User-ID + in: header + required: true + schema: + type: string + title: X-User-Id + - name: X-Workspace-ID + in: header + required: true + schema: + type: string + title: X-Workspace-Id + - name: X-Request-ID + in: header + required: false + schema: + anyOf: + - type: string + - type: 'null' + title: X-Request-Id + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateScheduleRequest' + responses: + '200': + description: Successful Response + content: + application/json: + schema: + type: object + additionalProperties: true + title: Response Update Schedule Api V1 Schedules Schedule Id Put + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + delete: + tags: + - schedules + summary: Delete Schedule + operationId: delete_schedule_api_v1_schedules__schedule_id__delete + parameters: + - name: schedule_id + in: path + required: true + schema: + type: string + title: Schedule Id + - name: X-User-ID + in: header + required: true + schema: + type: string + title: X-User-Id + - name: X-Workspace-ID + in: header + required: true + schema: + type: string + title: X-Workspace-Id + - name: X-Request-ID + in: header + required: false + schema: + anyOf: + - type: string + - type: 'null' + title: X-Request-Id + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/WorkflowVersionRequest' + responses: + '200': + description: Successful Response + content: + application/json: + schema: + type: object + additionalProperties: true + title: Response Delete Schedule Api V1 Schedules Schedule Id Delete + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + /api/v1/schedules/{schedule_id}/nodes: + post: + tags: + - schedules + summary: Create Schedule Node + operationId: create_schedule_node_api_v1_schedules__schedule_id__nodes_post + parameters: + - name: schedule_id + in: path + required: true + schema: + type: string + title: Schedule Id + - name: X-User-ID + in: header + required: true + schema: + type: string + title: X-User-Id + - name: X-Workspace-ID + in: header + required: true + schema: + type: string + title: X-Workspace-Id + - name: X-Request-ID + in: header + required: false + schema: + anyOf: + - type: string + - type: 'null' + title: X-Request-Id + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateScheduleNodeRequest' + responses: + '201': + description: Successful Response + content: + application/json: + schema: + type: object + additionalProperties: true + title: Response Create Schedule Node Api V1 Schedules Schedule Id Nodes Post + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + /api/v1/schedules/{schedule_id}/nodes/{node_id}: + put: + tags: + - schedules + summary: Update Schedule Node + operationId: update_schedule_node_api_v1_schedules__schedule_id__nodes__node_id__put + parameters: + - name: schedule_id + in: path + required: true + schema: + type: string + title: Schedule Id + - name: node_id + in: path + required: true + schema: + type: string + title: Node Id + - name: X-User-ID + in: header + required: true + schema: + type: string + title: X-User-Id + - name: X-Workspace-ID + in: header + required: true + schema: + type: string + title: X-Workspace-Id + - name: X-Request-ID + in: header + required: false + schema: + anyOf: + - type: string + - type: 'null' + title: X-Request-Id + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateScheduleNodeRequest' + responses: + '200': + description: Successful Response + content: + application/json: + schema: + type: object + additionalProperties: true + title: Response Update Schedule Node Api V1 Schedules Schedule Id Nodes Node Id Put + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + delete: + tags: + - schedules + summary: Delete Schedule Node + operationId: delete_schedule_node_api_v1_schedules__schedule_id__nodes__node_id__delete + parameters: + - name: schedule_id + in: path + required: true + schema: + type: string + title: Schedule Id + - name: node_id + in: path + required: true + schema: + type: string + title: Node Id + - name: X-User-ID + in: header + required: true + schema: + type: string + title: X-User-Id + - name: X-Workspace-ID + in: header + required: true + schema: + type: string + title: X-Workspace-Id + - name: X-Request-ID + in: header + required: false + schema: + anyOf: + - type: string + - type: 'null' + title: X-Request-Id + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/WorkflowVersionRequest' + responses: + '200': + description: Successful Response + content: + application/json: + schema: + type: object + additionalProperties: true + title: Response Delete Schedule Node Api V1 Schedules Schedule Id Nodes Node Id Delete + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + /api/v1/schedules/{schedule_id}/edges: + post: + tags: + - schedules + summary: Create Schedule Edge + operationId: create_schedule_edge_api_v1_schedules__schedule_id__edges_post + parameters: + - name: schedule_id + in: path + required: true + schema: + type: string + title: Schedule Id + - name: X-User-ID + in: header + required: true + schema: + type: string + title: X-User-Id + - name: X-Workspace-ID + in: header + required: true + schema: + type: string + title: X-Workspace-Id + - name: X-Request-ID + in: header + required: false + schema: + anyOf: + - type: string + - type: 'null' + title: X-Request-Id + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateScheduleEdgeRequest' + responses: + '201': + description: Successful Response + content: + application/json: + schema: + type: object + additionalProperties: true + title: Response Create Schedule Edge Api V1 Schedules Schedule Id Edges Post + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + /api/v1/schedules/{schedule_id}/edges/{edge_id}: + put: + tags: + - schedules + summary: Update Schedule Edge + operationId: update_schedule_edge_api_v1_schedules__schedule_id__edges__edge_id__put + parameters: + - name: schedule_id + in: path + required: true + schema: + type: string + title: Schedule Id + - name: edge_id + in: path + required: true + schema: + type: string + title: Edge Id + - name: X-User-ID + in: header + required: true + schema: + type: string + title: X-User-Id + - name: X-Workspace-ID + in: header + required: true + schema: + type: string + title: X-Workspace-Id + - name: X-Request-ID + in: header + required: false + schema: + anyOf: + - type: string + - type: 'null' + title: X-Request-Id + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateScheduleEdgeRequest' + responses: + '200': + description: Successful Response + content: + application/json: + schema: + type: object + additionalProperties: true + title: Response Update Schedule Edge Api V1 Schedules Schedule Id Edges Edge Id Put + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + delete: + tags: + - schedules + summary: Delete Schedule Edge + operationId: delete_schedule_edge_api_v1_schedules__schedule_id__edges__edge_id__delete + parameters: + - name: schedule_id + in: path + required: true + schema: + type: string + title: Schedule Id + - name: edge_id + in: path + required: true + schema: + type: string + title: Edge Id + - name: X-User-ID + in: header + required: true + schema: + type: string + title: X-User-Id + - name: X-Workspace-ID + in: header + required: true + schema: + type: string + title: X-Workspace-Id + - name: X-Request-ID + in: header + required: false + schema: + anyOf: + - type: string + - type: 'null' + title: X-Request-Id + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/WorkflowVersionRequest' + responses: + '200': + description: Successful Response + content: + application/json: + schema: + type: object + additionalProperties: true + title: Response Delete Schedule Edge Api V1 Schedules Schedule Id Edges Edge Id Delete + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + /api/v1/schedules/{schedule_id}/validate: + post: + tags: + - schedules + summary: Validate Schedule + operationId: validate_schedule_api_v1_schedules__schedule_id__validate_post + parameters: + - name: schedule_id + in: path + required: true + schema: + type: string + title: Schedule Id + - name: X-User-ID + in: header + required: true + schema: + type: string + title: X-User-Id + - name: X-Workspace-ID + in: header + required: true + schema: + type: string + title: X-Workspace-Id + - name: X-Request-ID + in: header + required: false + schema: + anyOf: + - type: string + - type: 'null' + title: X-Request-Id + responses: + '200': + description: Successful Response + content: + application/json: + schema: + type: object + additionalProperties: true + title: Response Validate Schedule Api V1 Schedules Schedule Id Validate Post + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + /api/v1/scripts: + post: + tags: + - scripts + summary: Create Script + operationId: create_script_api_v1_scripts_post + parameters: + - name: X-User-ID + in: header + required: true + schema: + type: string + title: X-User-Id + - name: X-Workspace-ID + in: header + required: true + schema: + type: string + title: X-Workspace-Id + - name: X-Request-ID + in: header + required: false + schema: + anyOf: + - type: string + - type: 'null' + title: X-Request-Id + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateScriptRequest' + responses: + '201': + description: Successful Response + content: + application/json: + schema: + type: object + additionalProperties: true + title: Response Create Script Api V1 Scripts Post + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + get: + tags: + - scripts + summary: List Scripts + operationId: list_scripts_api_v1_scripts_get + parameters: + - name: X-User-ID + in: header + required: true + schema: + type: string + title: X-User-Id + - name: X-Workspace-ID + in: header + required: true + schema: + type: string + title: X-Workspace-Id + - name: X-Request-ID + in: header + required: false + schema: + anyOf: + - type: string + - type: 'null' + title: X-Request-Id + responses: + '200': + description: Successful Response + content: + application/json: + schema: + type: object + additionalProperties: true + title: Response List Scripts Api V1 Scripts Get + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + /api/v1/scripts/upload: + post: + tags: + - scripts + summary: Upload Script + operationId: upload_script_api_v1_scripts_upload_post + parameters: + - name: file_name + in: query + required: true + schema: + type: string + minLength: 1 + maxLength: 255 + title: File Name + - name: parent_path + in: query + required: false + schema: + type: string + maxLength: 1024 + default: '' + title: Parent Path + - name: visibility + in: query + required: false + schema: + type: string + pattern: ^(private|workspace|public)$ + default: workspace + title: Visibility + - name: X-User-ID + in: header + required: true + schema: + type: string + title: X-User-Id + - name: X-Workspace-ID + in: header + required: true + schema: + type: string + title: X-Workspace-Id + - name: X-Request-ID + in: header + required: false + schema: + anyOf: + - type: string + - type: 'null' + title: X-Request-Id + responses: + '201': + description: Successful Response + content: + application/json: + schema: + type: object + additionalProperties: true + title: Response Upload Script Api V1 Scripts Upload Post + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + /api/v1/workspace-tree: + get: + tags: + - scripts + summary: Get Workspace Tree + operationId: get_workspace_tree_api_v1_workspace_tree_get + parameters: + - name: X-User-ID + in: header + required: true + schema: + type: string + title: X-User-Id + - name: X-Workspace-ID + in: header + required: true + schema: + type: string + title: X-Workspace-Id + - name: X-Request-ID + in: header + required: false + schema: + anyOf: + - type: string + - type: 'null' + title: X-Request-Id + responses: + '200': + description: Successful Response + content: + application/json: + schema: + type: object + additionalProperties: true + title: Response Get Workspace Tree Api V1 Workspace Tree Get + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + /api/v1/workspace-directories: + post: + tags: + - scripts + summary: Create Workspace Directory + operationId: create_workspace_directory_api_v1_workspace_directories_post + parameters: + - name: X-User-ID + in: header + required: true + schema: + type: string + title: X-User-Id + - name: X-Workspace-ID + in: header + required: true + schema: + type: string + title: X-Workspace-Id + - name: X-Request-ID + in: header + required: false + schema: + anyOf: + - type: string + - type: 'null' + title: X-Request-Id + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateWorkspaceDirectoryRequest' + responses: + '201': + description: Successful Response + content: + application/json: + schema: + type: object + additionalProperties: true + title: Response Create Workspace Directory Api V1 Workspace Directories Post + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + delete: + tags: + - scripts + summary: Delete Workspace Directory + operationId: delete_workspace_directory_api_v1_workspace_directories_delete + parameters: + - name: path + in: query + required: true + schema: + type: string + minLength: 1 + maxLength: 1024 + title: Path + - name: X-User-ID + in: header + required: true + schema: + type: string + title: X-User-Id + - name: X-Workspace-ID + in: header + required: true + schema: + type: string + title: X-Workspace-Id + - name: X-Request-ID + in: header + required: false + schema: + anyOf: + - type: string + - type: 'null' + title: X-Request-Id + responses: + '200': + description: Successful Response + content: + application/json: + schema: + type: object + additionalProperties: true + title: Response Delete Workspace Directory Api V1 Workspace Directories Delete + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + /api/v1/scripts/{script_id}: + get: + tags: + - scripts + summary: Get Script + operationId: get_script_api_v1_scripts__script_id__get + parameters: + - name: script_id + in: path + required: true + schema: + type: string + title: Script Id + - name: X-User-ID + in: header + required: true + schema: + type: string + title: X-User-Id + - name: X-Workspace-ID + in: header + required: true + schema: + type: string + title: X-Workspace-Id + - name: X-Request-ID + in: header + required: false + schema: + anyOf: + - type: string + - type: 'null' + title: X-Request-Id + responses: + '200': + description: Successful Response + content: + application/json: + schema: + type: object + additionalProperties: true + title: Response Get Script Api V1 Scripts Script Id Get + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + put: + tags: + - scripts + summary: Update Script + operationId: update_script_api_v1_scripts__script_id__put + parameters: + - name: script_id + in: path + required: true + schema: + type: string + title: Script Id + - name: X-User-ID + in: header + required: true + schema: + type: string + title: X-User-Id + - name: X-Workspace-ID + in: header + required: true + schema: + type: string + title: X-Workspace-Id + - name: X-Request-ID + in: header + required: false + schema: + anyOf: + - type: string + - type: 'null' + title: X-Request-Id + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateScriptRequest' + responses: + '200': + description: Successful Response + content: + application/json: + schema: + type: object + additionalProperties: true + title: Response Update Script Api V1 Scripts Script Id Put + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + delete: + tags: + - scripts + summary: Delete Script + operationId: delete_script_api_v1_scripts__script_id__delete + parameters: + - name: script_id + in: path + required: true + schema: + type: string + title: Script Id + - name: X-User-ID + in: header + required: true + schema: + type: string + title: X-User-Id + - name: X-Workspace-ID + in: header + required: true + schema: + type: string + title: X-Workspace-Id + - name: X-Request-ID + in: header + required: false + schema: + anyOf: + - type: string + - type: 'null' + title: X-Request-Id + responses: + '200': + description: Successful Response + content: + application/json: + schema: + type: object + additionalProperties: true + title: Response Delete Script Api V1 Scripts Script Id Delete + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + /api/v1/scripts/{script_id}/versions: + post: + tags: + - scripts + summary: Publish Version + operationId: publish_version_api_v1_scripts__script_id__versions_post + parameters: + - name: script_id + in: path + required: true + schema: + type: string + title: Script Id + - name: X-User-ID + in: header + required: true + schema: + type: string + title: X-User-Id + - name: X-Workspace-ID + in: header + required: true + schema: + type: string + title: X-Workspace-Id + - name: X-Request-ID + in: header + required: false + schema: + anyOf: + - type: string + - type: 'null' + title: X-Request-Id + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/PublishVersionRequest' + responses: + '201': + description: Successful Response + content: + application/json: + schema: + type: object + additionalProperties: true + title: Response Publish Version Api V1 Scripts Script Id Versions Post + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + get: + tags: + - scripts + summary: List Versions + operationId: list_versions_api_v1_scripts__script_id__versions_get + parameters: + - name: script_id + in: path + required: true + schema: + type: string + title: Script Id + - name: X-User-ID + in: header + required: true + schema: + type: string + title: X-User-Id + - name: X-Workspace-ID + in: header + required: true + schema: + type: string + title: X-Workspace-Id + - name: X-Request-ID + in: header + required: false + schema: + anyOf: + - type: string + - type: 'null' + title: X-Request-Id + responses: + '200': + description: Successful Response + content: + application/json: + schema: + type: object + additionalProperties: true + title: Response List Versions Api V1 Scripts Script Id Versions Get + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + /api/v1/versions/{versions_id}: + get: + tags: + - scripts + summary: Get Version + operationId: get_version_api_v1_versions__versions_id__get + parameters: + - name: versions_id + in: path + required: true + schema: + type: string + title: Versions Id + - name: X-User-ID + in: header + required: true + schema: + type: string + title: X-User-Id + - name: X-Workspace-ID + in: header + required: true + schema: + type: string + title: X-Workspace-Id + - name: X-Request-ID + in: header + required: false + schema: + anyOf: + - type: string + - type: 'null' + title: X-Request-Id + responses: + '200': + description: Successful Response + content: + application/json: + schema: + type: object + additionalProperties: true + title: Response Get Version Api V1 Versions Versions Id Get + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + delete: + tags: + - scripts + summary: Delete Version + operationId: delete_version_api_v1_versions__versions_id__delete + parameters: + - name: versions_id + in: path + required: true + schema: + type: string + title: Versions Id + - name: X-User-ID + in: header + required: true + schema: + type: string + title: X-User-Id + - name: X-Workspace-ID + in: header + required: true + schema: + type: string + title: X-Workspace-Id + - name: X-Request-ID + in: header + required: false + schema: + anyOf: + - type: string + - type: 'null' + title: X-Request-Id + responses: + '200': + description: Successful Response + content: + application/json: + schema: + type: object + additionalProperties: true + title: Response Delete Version Api V1 Versions Versions Id Delete + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + /api/v1/versions/{versions_id}/download-url: + post: + tags: + - scripts + summary: Version Download Url + operationId: version_download_url_api_v1_versions__versions_id__download_url_post + parameters: + - name: versions_id + in: path + required: true + schema: + type: string + title: Versions Id + - name: X-User-ID + in: header + required: true + schema: + type: string + title: X-User-Id + - name: X-Workspace-ID + in: header + required: true + schema: + type: string + title: X-Workspace-Id + - name: X-Request-ID + in: header + required: false + schema: + anyOf: + - type: string + - type: 'null' + title: X-Request-Id + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/DownloadUrlRequest' + responses: + '200': + description: Successful Response + content: + application/json: + schema: + type: object + additionalProperties: true + title: Response Version Download Url Api V1 Versions Versions Id Download Url Post + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + /api/v1/admin/employees: + get: + tags: + - admin + summary: List Employees + operationId: list_employees_api_v1_admin_employees_get + parameters: + - name: X-User-ID + in: header + required: true + schema: + type: string + title: X-User-Id + - name: X-Workspace-ID + in: header + required: true + schema: + type: string + title: X-Workspace-Id + - name: X-Request-ID + in: header + required: false + schema: + anyOf: + - type: string + - type: 'null' + title: X-Request-Id + responses: + '200': + description: Successful Response + content: + application/json: + schema: + type: object + additionalProperties: true + title: Response List Employees Api V1 Admin Employees Get + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + post: + tags: + - admin + summary: Create Employee + operationId: create_employee_api_v1_admin_employees_post + parameters: + - name: X-User-ID + in: header + required: true + schema: + type: string + title: X-User-Id + - name: X-Workspace-ID + in: header + required: true + schema: + type: string + title: X-Workspace-Id + - name: X-Request-ID + in: header + required: false + schema: + anyOf: + - type: string + - type: 'null' + title: X-Request-Id + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/EmployeeCreate' + responses: + '201': + description: Successful Response + content: + application/json: + schema: + type: object + additionalProperties: true + title: Response Create Employee Api V1 Admin Employees Post + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + /api/v1/admin/employees/{user_id}: + patch: + tags: + - admin + summary: Update Employee + operationId: update_employee_api_v1_admin_employees__user_id__patch + parameters: + - name: user_id + in: path + required: true + schema: + type: string + title: User Id + - name: X-User-ID + in: header + required: true + schema: + type: string + title: X-User-Id + - name: X-Workspace-ID + in: header + required: true + schema: + type: string + title: X-Workspace-Id + - name: X-Request-ID + in: header + required: false + schema: + anyOf: + - type: string + - type: 'null' + title: X-Request-Id + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/EmployeeUpdate' + responses: + '200': + description: Successful Response + content: + application/json: + schema: + type: object + additionalProperties: true + title: Response Update Employee Api V1 Admin Employees User Id Patch + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + delete: + tags: + - admin + summary: Delete Employee + operationId: delete_employee_api_v1_admin_employees__user_id__delete + parameters: + - name: user_id + in: path + required: true + schema: + type: string + title: User Id + - name: X-User-ID + in: header + required: true + schema: + type: string + title: X-User-Id + - name: X-Workspace-ID + in: header + required: true + schema: + type: string + title: X-Workspace-Id + - name: X-Request-ID + in: header + required: false + schema: + anyOf: + - type: string + - type: 'null' + title: X-Request-Id + responses: + '200': + description: Successful Response + content: + application/json: + schema: + type: object + additionalProperties: true + title: Response Delete Employee Api V1 Admin Employees User Id Delete + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' +components: + schemas: + CompleteResourceUploadRequest: + properties: + resource_name: + type: string + maxLength: 255 + minLength: 1 + title: Resource Name + description: + anyOf: + - type: string + maxLength: 1000 + - type: 'null' + title: Description + visibility: + type: string + enum: + - private + - workspace + - public + title: Visibility + default: private + additionalProperties: false + type: object + required: + - resource_name + title: CompleteResourceUploadRequest + CreateJupyterAccessTicketRequest: + properties: + edit_session_id: + type: string + maxLength: 26 + minLength: 26 + title: Edit Session Id + lock_token: + type: string + maxLength: 256 + minLength: 32 + title: Lock Token + additionalProperties: false + type: object + required: + - edit_session_id + - lock_token + title: CreateJupyterAccessTicketRequest + CreateResourceUploadRequest: + properties: + file_name: + type: string + maxLength: 255 + minLength: 1 + title: File Name + content_type: + type: string + maxLength: 255 + minLength: 1 + title: Content Type + expected_size_bytes: + type: integer + maximum: 104857600.0 + minimum: 0.0 + title: Expected Size Bytes + expected_hash: + anyOf: + - type: string + maxLength: 64 + minLength: 64 + - type: 'null' + title: Expected Hash + additionalProperties: false + type: object + required: + - file_name + - content_type + - expected_size_bytes + title: CreateResourceUploadRequest + CreateScheduleEdgeRequest: + properties: + workflow_version: + type: integer + minimum: 1.0 + title: Workflow Version + source_node_id: + type: string + maxLength: 26 + minLength: 26 + title: Source Node Id + target_node_id: + type: string + maxLength: 26 + minLength: 26 + title: Target Node Id + condition_expr: + anyOf: + - type: string + maxLength: 1000 + - type: 'null' + title: Condition Expr + additionalProperties: false + type: object + required: + - workflow_version + - source_node_id + - target_node_id + title: CreateScheduleEdgeRequest + CreateScheduleNodeRequest: + properties: + workflow_version: + type: integer + minimum: 1.0 + title: Workflow Version + node_key: + type: string + maxLength: 64 + minLength: 1 + pattern: ^[A-Za-z][A-Za-z0-9_-]*$ + title: Node Key + node_name: + type: string + maxLength: 255 + minLength: 1 + title: Node Name + versions_id: + type: string + maxLength: 26 + minLength: 26 + title: Versions Id + timeout_seconds: + type: integer + maximum: 86400.0 + minimum: 1.0 + title: Timeout Seconds + default: 600 + retry_count: + type: integer + maximum: 10.0 + minimum: 0.0 + title: Retry Count + default: 0 + retry_interval_sec: + type: integer + maximum: 3600.0 + minimum: 0.0 + title: Retry Interval Sec + default: 5 + position_x: + type: number + maximum: 100000.0 + minimum: -100000.0 + title: Position X + default: 0 + position_y: + type: number + maximum: 100000.0 + minimum: -100000.0 + title: Position Y + default: 0 + arguments_json: + additionalProperties: true + type: object + maxProperties: 100 + title: Arguments Json + env_refs_json: + additionalProperties: + type: string + type: object + maxProperties: 100 + title: Env Refs Json + additionalProperties: false + type: object + required: + - workflow_version + - node_key + - node_name + - versions_id + title: CreateScheduleNodeRequest + CreateScheduleRequest: + properties: + schedule_name: + type: string + maxLength: 255 + minLength: 1 + title: Schedule Name + description: + anyOf: + - type: string + maxLength: 1000 + - type: 'null' + title: Description + trigger_type: + type: string + enum: + - manual + - cron + - api + title: Trigger Type + default: cron + cron_expression: + anyOf: + - type: string + maxLength: 128 + - type: 'null' + title: Cron Expression + timezone: + type: string + maxLength: 64 + minLength: 1 + title: Timezone + default: Asia/Shanghai + enabled: + type: boolean + title: Enabled + default: false + max_concurrency: + type: integer + maximum: 64.0 + minimum: 1.0 + title: Max Concurrency + default: 1 + failure_policy: + type: string + enum: + - stop + - continue + title: Failure Policy + default: stop + additionalProperties: false + type: object + required: + - schedule_name + title: CreateScheduleRequest + CreateScriptRequest: + properties: + script_name: + type: string + maxLength: 255 + minLength: 1 + title: Script Name + script_type: + type: string + enum: + - python + - notebook + title: Script Type + content: + type: string + maxLength: 10485760 + title: Content + visibility: + type: string + enum: + - private + - workspace + - public + title: Visibility + default: private + parent_path: + anyOf: + - type: string + maxLength: 1024 + - type: 'null' + title: Parent Path + additionalProperties: false + type: object + required: + - script_name + - script_type + - content + title: CreateScriptRequest + CreateWorkspaceDirectoryRequest: + properties: + directory_name: + type: string + maxLength: 255 + minLength: 1 + title: Directory Name + parent_path: + type: string + maxLength: 1024 + title: Parent Path + default: '' + additionalProperties: false + type: object + required: + - directory_name + title: CreateWorkspaceDirectoryRequest + CronPreviewRequest: + properties: + cron_expression: + type: string + maxLength: 128 + minLength: 1 + title: Cron Expression + timezone: + type: string + maxLength: 64 + minLength: 1 + title: Timezone + default: Asia/Shanghai + count: + type: integer + maximum: 20.0 + minimum: 1.0 + title: Count + default: 5 + base_time: + anyOf: + - type: string + format: date-time + - type: 'null' + title: Base Time + additionalProperties: false + type: object + required: + - cron_expression + title: CronPreviewRequest + DownloadUrlRequest: + properties: + expires_seconds: + type: integer + maximum: 3600.0 + minimum: 30.0 + title: Expires Seconds + default: 300 + additionalProperties: false + type: object + title: DownloadUrlRequest + EmployeeCreate: + properties: + username: + type: string + maxLength: 64 + minLength: 2 + title: Username + display_name: + type: string + maxLength: 100 + minLength: 1 + title: Display Name + email: + anyOf: + - type: string + maxLength: 255 + - type: 'null' + title: Email + role_code: + type: string + enum: + - admin + - developer + title: Role Code + default: developer + additionalProperties: false + type: object + required: + - username + - display_name + title: EmployeeCreate + EmployeeUpdate: + properties: + display_name: + anyOf: + - type: string + maxLength: 100 + minLength: 1 + - type: 'null' + title: Display Name + email: + anyOf: + - type: string + maxLength: 255 + - type: 'null' + title: Email + role_code: + anyOf: + - type: string + enum: + - admin + - developer + - type: 'null' + title: Role Code + status: + anyOf: + - type: string + enum: + - active + - disabled + - locked + - type: 'null' + title: Status + additionalProperties: false + type: object + title: EmployeeUpdate + FileLockTokenRequest: + properties: + lock_token: + type: string + maxLength: 256 + minLength: 32 + title: Lock Token + additionalProperties: false + type: object + required: + - lock_token + title: FileLockTokenRequest + HTTPValidationError: + properties: + detail: + items: + $ref: '#/components/schemas/ValidationError' + type: array + title: Detail + type: object + title: HTTPValidationError + PublishVersionRequest: + properties: + source_object_id: + anyOf: + - type: string + maxLength: 26 + minLength: 26 + - type: 'null' + title: Source Object Id + release_note: + anyOf: + - type: string + maxLength: 1000 + - type: 'null' + title: Release Note + visibility: + type: string + enum: + - private + - workspace + - public + title: Visibility + default: workspace + additionalProperties: false + type: object + title: PublishVersionRequest + RunScheduleRequest: + properties: + reason: + type: string + maxLength: 255 + minLength: 1 + title: Reason + default: manual_run + additionalProperties: false + type: object + title: RunScheduleRequest + UpdateScheduleEdgeRequest: + properties: + workflow_version: + type: integer + minimum: 1.0 + title: Workflow Version + condition_expr: + anyOf: + - type: string + maxLength: 1000 + - type: 'null' + title: Condition Expr + additionalProperties: false + type: object + required: + - workflow_version + title: UpdateScheduleEdgeRequest + UpdateScheduleNodeRequest: + properties: + workflow_version: + type: integer + minimum: 1.0 + title: Workflow Version + node_name: + anyOf: + - type: string + maxLength: 255 + minLength: 1 + - type: 'null' + title: Node Name + versions_id: + anyOf: + - type: string + maxLength: 26 + minLength: 26 + - type: 'null' + title: Versions Id + timeout_seconds: + anyOf: + - type: integer + maximum: 86400.0 + minimum: 1.0 + - type: 'null' + title: Timeout Seconds + retry_count: + anyOf: + - type: integer + maximum: 10.0 + minimum: 0.0 + - type: 'null' + title: Retry Count + retry_interval_sec: + anyOf: + - type: integer + maximum: 3600.0 + minimum: 0.0 + - type: 'null' + title: Retry Interval Sec + position_x: + anyOf: + - type: number + maximum: 100000.0 + minimum: -100000.0 + - type: 'null' + title: Position X + position_y: + anyOf: + - type: number + maximum: 100000.0 + minimum: -100000.0 + - type: 'null' + title: Position Y + arguments_json: + anyOf: + - additionalProperties: true + type: object + maxProperties: 100 + - type: 'null' + title: Arguments Json + env_refs_json: + anyOf: + - additionalProperties: + type: string + type: object + maxProperties: 100 + - type: 'null' + title: Env Refs Json + additionalProperties: false + type: object + required: + - workflow_version + title: UpdateScheduleNodeRequest + UpdateScheduleRequest: + properties: + workflow_version: + type: integer + minimum: 1.0 + title: Workflow Version + schedule_name: + anyOf: + - type: string + maxLength: 255 + minLength: 1 + - type: 'null' + title: Schedule Name + description: + anyOf: + - type: string + maxLength: 1000 + - type: 'null' + title: Description + trigger_type: + anyOf: + - type: string + enum: + - manual + - cron + - api + - type: 'null' + title: Trigger Type + cron_expression: + anyOf: + - type: string + maxLength: 128 + - type: 'null' + title: Cron Expression + timezone: + anyOf: + - type: string + maxLength: 64 + minLength: 1 + - type: 'null' + title: Timezone + enabled: + anyOf: + - type: boolean + - type: 'null' + title: Enabled + max_concurrency: + anyOf: + - type: integer + maximum: 64.0 + minimum: 1.0 + - type: 'null' + title: Max Concurrency + failure_policy: + anyOf: + - type: string + enum: + - stop + - continue + - type: 'null' + title: Failure Policy + additionalProperties: false + type: object + required: + - workflow_version + title: UpdateScheduleRequest + UpdateScriptRequest: + properties: + content: + type: string + maxLength: 10485760 + title: Content + additionalProperties: false + type: object + required: + - content + title: UpdateScriptRequest + ValidationError: + properties: + loc: + items: + anyOf: + - type: string + - type: integer + type: array + title: Location + msg: + type: string + title: Message + type: + type: string + title: Error Type + type: object + required: + - loc + - msg + - type + title: ValidationError + WorkflowVersionRequest: + properties: + workflow_version: + type: integer + minimum: 1.0 + title: Workflow Version + additionalProperties: false + type: object + required: + - workflow_version + title: WorkflowVersionRequest diff --git a/contracts/openapi/runtime-api-internal-v1.yaml b/contracts/openapi/runtime-api-internal-v1.yaml new file mode 100644 index 0000000..85b7727 --- /dev/null +++ b/contracts/openapi/runtime-api-internal-v1.yaml @@ -0,0 +1,699 @@ +openapi: 3.1.0 +info: + title: runtime-manager service + version: 0.1.0 +paths: + /: + get: + summary: Root + operationId: root__get + responses: + '200': + description: Successful Response + content: + application/json: + schema: + additionalProperties: + type: string + type: object + title: Response Root Get + /health/live: + get: + summary: Live + operationId: live_health_live_get + responses: + '200': + description: Successful Response + content: + application/json: + schema: + additionalProperties: + type: string + type: object + title: Response Live Health Live Get + /api/v1/health: + get: + summary: Public Health + operationId: public_health_api_v1_health_get + responses: + '200': + description: Successful Response + content: + application/json: + schema: + additionalProperties: + type: string + type: object + title: Response Public Health Api V1 Health Get + /health/ready: + get: + summary: Ready + operationId: ready_health_ready_get + responses: + '200': + description: Successful Response + content: + application/json: + schema: + additionalProperties: true + type: object + title: Response Ready Health Ready Get + /internal/v1/file-locks/acquire: + post: + summary: Acquire File Lock + operationId: acquire_file_lock_internal_v1_file_locks_acquire_post + parameters: + - name: X-Service-Token + in: header + required: true + schema: + type: string + title: X-Service-Token + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/AcquireFileLockRequest' + responses: + '201': + description: Successful Response + content: + application/json: + schema: + type: object + additionalProperties: true + title: Response Acquire File Lock Internal V1 File Locks Acquire Post + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + /internal/v1/file-locks/{edit_session_id}/heartbeat: + post: + summary: Heartbeat File Lock + operationId: heartbeat_file_lock_internal_v1_file_locks__edit_session_id__heartbeat_post + parameters: + - name: edit_session_id + in: path + required: true + schema: + type: string + title: Edit Session Id + - name: X-Service-Token + in: header + required: true + schema: + type: string + title: X-Service-Token + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/FileLockTokenRequest' + responses: + '200': + description: Successful Response + content: + application/json: + schema: + type: object + additionalProperties: true + title: Response Heartbeat File Lock Internal V1 File Locks Edit Session Id Heartbeat + Post + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + /internal/v1/file-locks/{edit_session_id}: + delete: + summary: Release File Lock + operationId: release_file_lock_internal_v1_file_locks__edit_session_id__delete + parameters: + - name: edit_session_id + in: path + required: true + schema: + type: string + title: Edit Session Id + - name: X-Service-Token + in: header + required: true + schema: + type: string + title: X-Service-Token + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/FileLockTokenRequest' + responses: + '200': + description: Successful Response + content: + application/json: + schema: + type: object + additionalProperties: true + title: Response Release File Lock Internal V1 File Locks Edit Session Id Delete + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + /internal/v1/jupyter/access-tickets/{edit_session_id}: + post: + summary: Create Jupyter Access Ticket + operationId: create_jupyter_access_ticket_internal_v1_jupyter_access_tickets__edit_session_id__post + parameters: + - name: edit_session_id + in: path + required: true + schema: + type: string + title: Edit Session Id + - name: X-Service-Token + in: header + required: true + schema: + type: string + title: X-Service-Token + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/FileLockTokenRequest' + responses: + '201': + description: Successful Response + content: + application/json: + schema: + type: object + additionalProperties: true + title: Response Create Jupyter Access Ticket Internal V1 Jupyter Access Tickets Edit + Session Id Post + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + /internal/v1/jupyter/authorize: + get: + summary: Authorize Jupyter Proxy + operationId: authorize_jupyter_proxy_internal_v1_jupyter_authorize_get + parameters: + - name: X-Original-URI + in: header + required: true + schema: + type: string + title: X-Original-Uri + - name: X-Service-Token + in: header + required: true + schema: + type: string + title: X-Service-Token + - name: jupyter_access + in: cookie + required: false + schema: + anyOf: + - type: string + - type: 'null' + title: Jupyter Access + responses: + '204': + description: Successful Response + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + /internal/v1/runtimes/ensure: + post: + summary: Ensure Runtime + operationId: ensure_runtime_internal_v1_runtimes_ensure_post + parameters: + - name: X-Service-Token + in: header + required: true + schema: + type: string + title: X-Service-Token + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/EnsureRuntimeApiRequest' + responses: + '200': + description: Successful Response + content: + application/json: + schema: + type: object + additionalProperties: true + title: Response Ensure Runtime Internal V1 Runtimes Ensure Post + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + /internal/v1/runtimes/{runtime_id}: + get: + summary: Get Runtime + operationId: get_runtime_internal_v1_runtimes__runtime_id__get + parameters: + - name: runtime_id + in: path + required: true + schema: + type: string + title: Runtime Id + - name: workspace_id + in: query + required: true + schema: + type: string + minLength: 26 + maxLength: 26 + title: Workspace Id + - name: user_id + in: query + required: true + schema: + type: string + minLength: 26 + maxLength: 26 + title: User Id + - name: X-Service-Token + in: header + required: true + schema: + type: string + title: X-Service-Token + responses: + '200': + description: Successful Response + content: + application/json: + schema: + type: object + additionalProperties: true + title: Response Get Runtime Internal V1 Runtimes Runtime Id Get + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + delete: + summary: Stop Runtime + operationId: stop_runtime_internal_v1_runtimes__runtime_id__delete + parameters: + - name: runtime_id + in: path + required: true + schema: + type: string + title: Runtime Id + - name: X-Service-Token + in: header + required: true + schema: + type: string + title: X-Service-Token + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/StopRuntimeApiRequest' + responses: + '200': + description: Successful Response + content: + application/json: + schema: + type: object + additionalProperties: true + title: Response Stop Runtime Internal V1 Runtimes Runtime Id Delete + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + /internal/v1/runtimes/{runtime_id}/restart: + post: + summary: Restart Runtime + operationId: restart_runtime_internal_v1_runtimes__runtime_id__restart_post + parameters: + - name: runtime_id + in: path + required: true + schema: + type: string + title: Runtime Id + - name: X-Service-Token + in: header + required: true + schema: + type: string + title: X-Service-Token + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/RuntimeIdentityRequest' + responses: + '200': + description: Successful Response + content: + application/json: + schema: + type: object + additionalProperties: true + title: Response Restart Runtime Internal V1 Runtimes Runtime Id Restart Post + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + /internal/v1/runtimes/{runtime_id}/sessions: + post: + summary: Create Runtime Session + operationId: create_runtime_session_internal_v1_runtimes__runtime_id__sessions_post + parameters: + - name: runtime_id + in: path + required: true + schema: + type: string + title: Runtime Id + - name: X-Service-Token + in: header + required: true + schema: + type: string + title: X-Service-Token + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateRuntimeSessionApiRequest' + responses: + '201': + description: Successful Response + content: + application/json: + schema: + type: object + additionalProperties: true + title: Response Create Runtime Session Internal V1 Runtimes Runtime Id Sessions Post + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + /internal/v1/runtimes/{runtime_id}/sessions/{session_id}: + delete: + summary: Terminate Runtime Session + operationId: terminate_runtime_session_internal_v1_runtimes__runtime_id__sessions__session_id__delete + parameters: + - name: runtime_id + in: path + required: true + schema: + type: string + title: Runtime Id + - name: session_id + in: path + required: true + schema: + type: string + title: Session Id + - name: X-Service-Token + in: header + required: true + schema: + type: string + title: X-Service-Token + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/RuntimeIdentityRequest' + responses: + '200': + description: Successful Response + content: + application/json: + schema: + type: object + additionalProperties: true + title: Response Terminate Runtime Session Internal V1 Runtimes Runtime Id Sessions Session + Id Delete + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + /internal/health/runtime: + get: + summary: Internal Health + operationId: internal_health_internal_health_runtime_get + responses: + '200': + description: Successful Response + content: + application/json: + schema: + additionalProperties: + type: string + type: object + title: Response Internal Health Internal Health Runtime Get +components: + schemas: + AcquireFileLockRequest: + properties: + workspace_id: + type: string + maxLength: 26 + minLength: 26 + title: Workspace Id + storage_object_id: + type: string + maxLength: 26 + minLength: 26 + title: Storage Object Id + user_id: + type: string + maxLength: 26 + minLength: 26 + title: User Id + request_id: + anyOf: + - type: string + maxLength: 64 + minLength: 1 + - type: 'null' + title: Request Id + additionalProperties: false + type: object + required: + - workspace_id + - storage_object_id + - user_id + title: AcquireFileLockRequest + CreateRuntimeSessionApiRequest: + properties: + workspace_id: + type: string + maxLength: 26 + minLength: 26 + title: Workspace Id + user_id: + type: string + maxLength: 26 + minLength: 26 + title: User Id + request_id: + anyOf: + - type: string + maxLength: 64 + minLength: 1 + - type: 'null' + title: Request Id + storage_object_id: + type: string + maxLength: 26 + minLength: 26 + title: Storage Object Id + relative_path: + type: string + maxLength: 1024 + minLength: 1 + title: Relative Path + additionalProperties: false + type: object + required: + - workspace_id + - user_id + - storage_object_id + - relative_path + title: CreateRuntimeSessionApiRequest + EnsureRuntimeApiRequest: + properties: + workspace_id: + type: string + maxLength: 26 + minLength: 26 + title: Workspace Id + user_id: + type: string + maxLength: 26 + minLength: 26 + title: User Id + request_id: + anyOf: + - type: string + maxLength: 64 + minLength: 1 + - type: 'null' + title: Request Id + additionalProperties: false + type: object + required: + - workspace_id + - user_id + title: EnsureRuntimeApiRequest + FileLockTokenRequest: + properties: + workspace_id: + type: string + maxLength: 26 + minLength: 26 + title: Workspace Id + user_id: + type: string + maxLength: 26 + minLength: 26 + title: User Id + lock_token: + type: string + maxLength: 256 + minLength: 32 + title: Lock Token + additionalProperties: false + type: object + required: + - workspace_id + - user_id + - lock_token + title: FileLockTokenRequest + HTTPValidationError: + properties: + detail: + items: + $ref: '#/components/schemas/ValidationError' + type: array + title: Detail + type: object + title: HTTPValidationError + RuntimeIdentityRequest: + properties: + workspace_id: + type: string + maxLength: 26 + minLength: 26 + title: Workspace Id + user_id: + type: string + maxLength: 26 + minLength: 26 + title: User Id + request_id: + anyOf: + - type: string + maxLength: 64 + minLength: 1 + - type: 'null' + title: Request Id + additionalProperties: false + type: object + required: + - workspace_id + - user_id + title: RuntimeIdentityRequest + StopRuntimeApiRequest: + properties: + workspace_id: + type: string + maxLength: 26 + minLength: 26 + title: Workspace Id + user_id: + type: string + maxLength: 26 + minLength: 26 + title: User Id + request_id: + anyOf: + - type: string + maxLength: 64 + minLength: 1 + - type: 'null' + title: Request Id + reason: + type: string + maxLength: 64 + minLength: 1 + title: Reason + default: client_request + additionalProperties: false + type: object + required: + - workspace_id + - user_id + title: StopRuntimeApiRequest + ValidationError: + properties: + loc: + items: + anyOf: + - type: string + - type: integer + type: array + title: Location + msg: + type: string + title: Message + type: + type: string + title: Error Type + type: object + required: + - loc + - msg + - type + title: ValidationError diff --git a/contracts/openapi/storage-api-internal-v1.yaml b/contracts/openapi/storage-api-internal-v1.yaml new file mode 100644 index 0000000..17c3f0f --- /dev/null +++ b/contracts/openapi/storage-api-internal-v1.yaml @@ -0,0 +1,550 @@ +openapi: 3.1.0 +info: + title: storage-api service + version: 0.1.0 +paths: + /: + get: + summary: Root + operationId: root__get + responses: + '200': + description: Successful Response + content: + application/json: + schema: + additionalProperties: + type: string + type: object + title: Response Root Get + /health/live: + get: + summary: Live + operationId: live_health_live_get + responses: + '200': + description: Successful Response + content: + application/json: + schema: + additionalProperties: + type: string + type: object + title: Response Live Health Live Get + /api/v1/health: + get: + summary: Public Health + operationId: public_health_api_v1_health_get + responses: + '200': + description: Successful Response + content: + application/json: + schema: + additionalProperties: + type: string + type: object + title: Response Public Health Api V1 Health Get + /health/ready: + get: + summary: Ready + operationId: ready_health_ready_get + responses: + '200': + description: Successful Response + content: + application/json: + schema: + additionalProperties: true + type: object + title: Response Ready Health Ready Get + /internal/v1/uploads: + post: + summary: Create Upload + operationId: create_upload_internal_v1_uploads_post + parameters: + - name: X-Service-Token + in: header + required: true + schema: + type: string + title: X-Service-Token + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateUploadRequest' + responses: + '200': + description: Successful Response + content: + application/json: + schema: + type: object + additionalProperties: true + title: Response Create Upload Internal V1 Uploads Post + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + /internal/v1/uploads/{upload_id}/complete: + post: + summary: Complete Upload + operationId: complete_upload_internal_v1_uploads__upload_id__complete_post + parameters: + - name: upload_id + in: path + required: true + schema: + type: string + title: Upload Id + - name: X-Service-Token + in: header + required: true + schema: + type: string + title: X-Service-Token + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CompleteUploadRequest' + responses: + '200': + description: Successful Response + content: + application/json: + schema: + type: object + additionalProperties: true + title: Response Complete Upload Internal V1 Uploads Upload Id Complete Post + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + /internal/v1/uploads/{upload_id}/abort: + post: + summary: Abort Upload + operationId: abort_upload_internal_v1_uploads__upload_id__abort_post + parameters: + - name: upload_id + in: path + required: true + schema: + type: string + title: Upload Id + - name: X-Service-Token + in: header + required: true + schema: + type: string + title: X-Service-Token + responses: + '200': + description: Successful Response + content: + application/json: + schema: + type: object + additionalProperties: true + title: Response Abort Upload Internal V1 Uploads Upload Id Abort Post + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + /internal/v1/objects: + post: + summary: Create Server Object + operationId: create_server_object_internal_v1_objects_post + parameters: + - name: X-Service-Token + in: header + required: true + schema: + type: string + title: X-Service-Token + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ServerObjectRequest' + responses: + '200': + description: Successful Response + content: + application/json: + schema: + type: object + additionalProperties: true + title: Response Create Server Object Internal V1 Objects Post + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + /internal/v1/workspace-objects: + post: + summary: Register Workspace Object + operationId: register_workspace_object_internal_v1_workspace_objects_post + parameters: + - name: X-Service-Token + in: header + required: true + schema: + type: string + title: X-Service-Token + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/RegisterWorkspaceObjectRequest' + responses: + '200': + description: Successful Response + content: + application/json: + schema: + type: object + additionalProperties: true + title: Response Register Workspace Object Internal V1 Workspace Objects Post + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + /internal/v1/objects/{storage_object_id}/download-url: + post: + summary: Create Download Url + operationId: create_download_url_internal_v1_objects__storage_object_id__download_url_post + parameters: + - name: storage_object_id + in: path + required: true + schema: + type: string + title: Storage Object Id + - name: X-Service-Token + in: header + required: true + schema: + type: string + title: X-Service-Token + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/DownloadUrlRequest' + responses: + '200': + description: Successful Response + content: + application/json: + schema: + type: object + additionalProperties: true + title: Response Create Download Url Internal V1 Objects Storage Object Id Download Url + Post + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + /internal/v1/objects/{storage_object_id}: + delete: + summary: Delete Object + operationId: delete_object_internal_v1_objects__storage_object_id__delete + parameters: + - name: storage_object_id + in: path + required: true + schema: + type: string + title: Storage Object Id + - name: X-Service-Token + in: header + required: true + schema: + type: string + title: X-Service-Token + responses: + '200': + description: Successful Response + content: + application/json: + schema: + type: object + additionalProperties: true + title: Response Delete Object Internal V1 Objects Storage Object Id Delete + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + /internal/health/storage: + get: + summary: Internal Health + operationId: internal_health_internal_health_storage_get + responses: + '200': + description: Successful Response + content: + application/json: + schema: + additionalProperties: + type: string + type: object + title: Response Internal Health Internal Health Storage Get +components: + schemas: + CompleteUploadRequest: + properties: + usage_type: + type: string + enum: + - data_resource + - version_artifact + - snapshot + - run_log + - run_result + title: Usage Type + visibility: + type: string + enum: + - private + - workspace + - public + title: Visibility + default: private + is_immutable: + type: boolean + title: Is Immutable + default: false + additionalProperties: false + type: object + required: + - usage_type + title: CompleteUploadRequest + CreateUploadRequest: + properties: + workspace_id: + type: string + maxLength: 26 + minLength: 26 + title: Workspace Id + user_id: + type: string + maxLength: 26 + minLength: 26 + title: User Id + usage_type: + type: string + enum: + - data_resource + - version_artifact + - snapshot + - run_log + - run_result + title: Usage Type + file_name: + type: string + maxLength: 255 + minLength: 1 + title: File Name + content_type: + type: string + maxLength: 255 + minLength: 1 + title: Content Type + expected_size_bytes: + type: integer + maximum: 104857600.0 + minimum: 0.0 + title: Expected Size Bytes + expected_hash: + anyOf: + - type: string + maxLength: 64 + minLength: 64 + - type: 'null' + title: Expected Hash + idempotency_key: + type: string + maxLength: 128 + minLength: 8 + title: Idempotency Key + url_scope: + type: string + enum: + - public + - internal + title: Url Scope + default: public + additionalProperties: false + type: object + required: + - workspace_id + - user_id + - usage_type + - file_name + - content_type + - expected_size_bytes + - idempotency_key + title: CreateUploadRequest + DownloadUrlRequest: + properties: + expires_seconds: + type: integer + maximum: 3600.0 + minimum: 30.0 + title: Expires Seconds + default: 300 + additionalProperties: false + type: object + title: DownloadUrlRequest + HTTPValidationError: + properties: + detail: + items: + $ref: '#/components/schemas/ValidationError' + type: array + title: Detail + type: object + title: HTTPValidationError + RegisterWorkspaceObjectRequest: + properties: + workspace_id: + type: string + maxLength: 26 + minLength: 26 + title: Workspace Id + user_id: + type: string + maxLength: 26 + minLength: 26 + title: User Id + relative_path: + type: string + maxLength: 1024 + minLength: 1 + title: Relative Path + usage_type: + type: string + enum: + - working_copy + - public_script + title: Usage Type + visibility: + type: string + enum: + - private + - workspace + - public + title: Visibility + default: private + additionalProperties: false + type: object + required: + - workspace_id + - user_id + - relative_path + - usage_type + title: RegisterWorkspaceObjectRequest + ServerObjectRequest: + properties: + workspace_id: + type: string + maxLength: 26 + minLength: 26 + title: Workspace Id + user_id: + type: string + maxLength: 26 + minLength: 26 + title: User Id + usage_type: + type: string + enum: + - data_resource + - version_artifact + - snapshot + - run_log + - run_result + title: Usage Type + file_name: + type: string + maxLength: 255 + minLength: 1 + title: File Name + content_type: + type: string + maxLength: 255 + minLength: 1 + title: Content Type + content_base64: + type: string + minLength: 1 + title: Content Base64 + visibility: + type: string + enum: + - private + - workspace + - public + title: Visibility + default: private + is_immutable: + type: boolean + title: Is Immutable + default: false + idempotency_key: + type: string + maxLength: 128 + minLength: 8 + title: Idempotency Key + additionalProperties: false + type: object + required: + - workspace_id + - user_id + - usage_type + - file_name + - content_type + - content_base64 + - idempotency_key + title: ServerObjectRequest + ValidationError: + properties: + loc: + items: + anyOf: + - type: string + - type: integer + type: array + title: Location + msg: + type: string + title: Message + type: + type: string + title: Error Type + type: object + required: + - loc + - msg + - type + title: ValidationError diff --git a/contracts/runtime/README.md b/contracts/runtime/README.md new file mode 100644 index 0000000..4670133 --- /dev/null +++ b/contracts/runtime/README.md @@ -0,0 +1,26 @@ +# Runtime Adapter 契约 + +`runtime_adapter.py` 是 Runtime Manager 与具体运行环境之间的可执行契约。 + +当前 Compose 环境使用 `SharedJupyterAdapter`。它把共享 Jupyter Server 作为 +首期 Provider,并在 MySQL 中为每个 `Workspace` 建立并复用一个逻辑 +Runtime。每个打开的 Notebook 在该 Runtime 中建立独立 Jupyter Session, +每个 Session 绑定自己的 Kernel。 +后续 Docker/Kubernetes Provider 必须实现同一 Protocol,公共锁接口和 +`runtime_instances` 状态模型保持不变。 + +核心约束: + +- `ensure_running` 对同一作用域必须幂等; +- Runtime 的唯一复用作用域是 Workspace,不能按用户重复创建; +- Runtime 的期望状态、实际状态和租约写入 MySQL; +- Notebook 对应独立 Jupyter Session/Kernel,其创建和终止只能由 Runtime + Manager 调用; +- Provider 不能依赖 Platform API 的进程内状态; +- `proxy_base_path` 与 Jupyter 内部 `base_url` 必须一致。 +- Provider 调用必须携带内部服务凭据,不能通过关闭 XSRF 检查规避认证; +- Workspace 的目录、文件权限必须同时满足 Platform 原子写入和 Jupyter + 读写,当前 Compose 环境使用共享 Unix 组完成。 + +Jupyter 浏览器代理、访问票据和 WebSocket 约束见 +`jupyter-proxy-v1.md`。 diff --git a/contracts/runtime/__init__.py b/contracts/runtime/__init__.py new file mode 100644 index 0000000..22ae10c --- /dev/null +++ b/contracts/runtime/__init__.py @@ -0,0 +1 @@ +"""Runtime adapter contract package.""" diff --git a/contracts/runtime/jupyter-proxy-v1.md b/contracts/runtime/jupyter-proxy-v1.md new file mode 100644 index 0000000..bb58546 --- /dev/null +++ b/contracts/runtime/jupyter-proxy-v1.md @@ -0,0 +1,65 @@ +# Jupyter 代理与访问票据契约 V1 + +## 1. 浏览器访问流程 + +1. 前端携带用户、Workspace、`edit_session_id` 和 `lock_token` 调用 + `POST /api/v1/jupyter/access-tickets`。 +2. Platform API 校验编辑锁、Runtime 与 Jupyter Session 的归属及状态。 +3. 成功后返回 `/jupyter/...` 地址,并设置短期 + `jupyter_access` Cookie。 +4. 前端在当前平台页面的编辑区内嵌同源 `/jupyter/...` 页面,不新开浏览器 + 标签页。 +5. 内嵌页面经 Nginx 访问 Jupyter HTTP/WebSocket;Nginx 先执行内部鉴权 + 子请求,再把内部 Token 注入上游请求。 + +一个 Workspace 只建立或复用一个 Jupyter Runtime/Server;同一 Server +中的每个 Notebook 使用独立 Jupyter Session 和独立 Kernel。 + +浏览器响应、URL、JavaScript 和日志中都不得出现 Jupyter 内部 Token。 + +## 2. 访问票据 + +- 票据绑定:`user_id`、`workspace_id`、`edit_session_id`、 + `runtime_id`、`jupyter_session_id`。 +- 默认有效期:60 秒;最长不超过 5 分钟。 +- Cookie:`HttpOnly; SameSite=Lax; Path=/jupyter/`,生产环境必须增加 + `Secure`。 +- 票据只授权当前 Workspace 的 Jupyter 路径,不能跨 Workspace 使用。 +- Jupyter 页面必须与平台使用同一站点入口,并允许同源 iframe 嵌入。 + +## 3. Nginx 内部鉴权接口 + +```http +GET /internal/v1/jupyter/authorize +Cookie: jupyter_access= +X-Original-URI: /jupyter/... +X-Request-ID: +``` + +该接口只允许 Nginx 在内部网络调用: + +- `204`:票据有效,响应头提供上游地址和内部认证信息。 +- `401`:票据缺失、伪造或过期。 +- `403`:用户、Workspace、Session 不匹配,或编辑锁/Runtime 已失效。 + +成功响应头: + +```http +X-Jupyter-Upstream: http://jupyter:8888 +X-Jupyter-Authorization: token +X-Workspace-ID: +``` + +Nginx 必须删除客户端传入的 `Authorization`,使用内部鉴权结果重建上游 +认证头。 + +## 4. WebSocket 约束 + +- `/jupyter/api/kernels/*/channels` 与其他 WebSocket 路径使用 HTTP/1.1。 +- 转发 `Upgrade`、`Connection`、`Host`、`Origin` 和请求 ID。 +- 关闭代理缓冲,读超时不小于 3600 秒。 +- HTTP 和 WebSocket 使用同一票据校验规则。 +- 连接关闭只断开交互连接,不自动停止 Workspace Runtime。 +- Compose Demo 使用最长 5 分钟票据;前端必须继续发送编辑锁心跳,并在 + 票据到期前重新签发。 +- Jupyter Lab HTML 中出现的内部 Token 必须由 Nginx 在返回浏览器前清除。 diff --git a/contracts/runtime/runtime_adapter.py b/contracts/runtime/runtime_adapter.py new file mode 100644 index 0000000..478dae0 --- /dev/null +++ b/contracts/runtime/runtime_adapter.py @@ -0,0 +1,70 @@ +from __future__ import annotations + +from dataclasses import dataclass +from datetime import datetime +from typing import Protocol + + +@dataclass(frozen=True) +class EnsureRuntimeRequest: + runtime_id: str + workspace_id: str + workspace_code: str + owner_user_id: str + + +@dataclass(frozen=True) +class RuntimeEndpoint: + runtime_id: str + runtime_type: str + provider: str + runtime_ref: str + internal_url: str + proxy_base_path: str + + +@dataclass(frozen=True) +class RuntimeHealth: + runtime_id: str + healthy: bool + checked_at: datetime + detail: str | None = None + + +@dataclass(frozen=True) +class CreateSessionRequest: + runtime_id: str + workspace_code: str + relative_path: str + + +@dataclass(frozen=True) +class RuntimeSession: + runtime_id: str + session_id: str + jupyter_url: str + reused: bool + + +class RuntimeAdapter(Protocol): + async def ensure_running( + self, + request: EnsureRuntimeRequest, + ) -> RuntimeEndpoint: ... + + async def stop(self, runtime_id: str, reason: str) -> None: ... + + async def restart(self, runtime_id: str) -> RuntimeEndpoint: ... + + async def health(self, runtime_id: str) -> RuntimeHealth: ... + + async def create_session( + self, + request: CreateSessionRequest, + ) -> RuntimeSession: ... + + async def terminate_session( + self, + runtime_id: str, + session_id: str, + ) -> None: ... diff --git a/contracts/schedules/schedule-definition-api-v1.md b/contracts/schedules/schedule-definition-api-v1.md new file mode 100644 index 0000000..af6c88a --- /dev/null +++ b/contracts/schedules/schedule-definition-api-v1.md @@ -0,0 +1,122 @@ +# 调度定义 API 契约 V1 + +冻结日期:2026-07-28 +状态:`implemented` + +## 1. 范围 + +本契约只覆盖调度定义,不触发任务执行: + +- 调度方案增删改查; +- 稳定版本制品列表; +- 节点和连线增删改; +- 五段 Cron 校验与未来时间预览; +- DAG 完整性和有向无环校验。 + +立即运行、Cron 自动触发、Redis Streams 事件和 Worker 执行属于后续小步。 + +## 2. 请求上下文 + +所有接口都要求: + +| Header | 含义 | +|---|---| +| `X-User-ID` | 当前用户 | +| `X-Workspace-ID` | 当前 Workspace | +| `X-Request-ID` | 请求追踪 ID;可省略,由服务端生成 | + +服务端只返回当前 Workspace 的调度。节点引用的 `versions_id` 必须属于当前 +Workspace,且当前用户有权读取。 + +## 3. HTTP 接口 + +| 方法 | 路径 | 作用 | +|---|---|---| +| `GET` | `/api/v1/schedule-artifacts` | 查询可加入调度的稳定版本 | +| `POST` | `/api/v1/cron/preview` | 校验 Cron 并预览未来时间 | +| `GET` | `/api/v1/schedules` | 查询调度列表 | +| `POST` | `/api/v1/schedules` | 新建调度 | +| `GET` | `/api/v1/schedules/{schedule_id}` | 查询调度及完整 DAG | +| `PUT/PATCH` | `/api/v1/schedules/{schedule_id}` | 修改调度基本信息 | +| `DELETE` | `/api/v1/schedules/{schedule_id}` | 软删除调度 | +| `POST` | `/api/v1/schedules/{schedule_id}/nodes` | 新建节点 | +| `PUT` | `/api/v1/schedules/{schedule_id}/nodes/{node_id}` | 修改节点 | +| `DELETE` | `/api/v1/schedules/{schedule_id}/nodes/{node_id}` | 删除节点及关联连线 | +| `POST` | `/api/v1/schedules/{schedule_id}/edges` | 新建有向连线 | +| `PUT` | `/api/v1/schedules/{schedule_id}/edges/{edge_id}` | 修改连线条件 | +| `DELETE` | `/api/v1/schedules/{schedule_id}/edges/{edge_id}` | 删除连线 | +| `POST` | `/api/v1/schedules/{schedule_id}/validate` | 校验当前 DAG | + +成功响应统一为: + +```json +{ + "request_id": "01...", + "data": {}, + "meta": {} +} +``` + +## 4. 并发修改契约 + +新建调度时 `workflow_version=1`。每次修改调度、节点或连线都必须在请求体中 +提交当前 `workflow_version`,成功后版本号加一。 + +版本不一致时返回 `412 Precondition Failed`: + +```json +{ + "detail": { + "code": "WORKFLOW_VERSION_CONFLICT", + "message": "schedule was modified by another request", + "expected": 3, + "current": 4 + } +} +``` + +前端收到 `412` 后必须重新读取调度,不得用旧画布直接覆盖。 + +## 5. 调度与 Cron 约束 + +- `trigger_type`:`manual / cron / api`; +- `failure_policy`:`stop / continue`; +- Cron 固定为五段:`minute hour day month weekday`; +- 时区使用 IANA 名称,例如 `Asia/Shanghai`; +- `cron` 类型必须提供 `cron_expression`,其他类型不得提供; +- 新建的空调度不能直接启用; +- 只有 DAG 校验通过的调度才能设为 `enabled=true`; +- `next_run_at` 在数据库和接口中按 UTC 保存和返回。 + +Cron 预览请求示例: + +```json +{ + "cron_expression": "*/5 * * * *", + "timezone": "Asia/Shanghai", + "count": 5, + "base_time": "2026-07-28T08:00:00+08:00" +} +``` + +## 6. 节点与 DAG 约束 + +- 节点必须引用不可变稳定版本 `versions_id`,不能引用工作副本; +- 同一个调度内 `node_key` 唯一; +- 节点保存超时、重试、位置、参数和环境引用; +- 连线的起点、终点必须属于同一调度; +- 不允许自环、重复连线和有向环; +- 删除节点会同时删除与该节点相连的边; +- 已有运行历史的节点不能物理删除; +- 校验结果返回根节点、叶节点、拓扑顺序和错误列表。 + +## 7. 典型状态码 + +| 状态码 | 场景 | +|---|---| +| `201` | 调度、节点或连线创建成功 | +| `404` | 调度、稳定版本、节点或连线不存在/不可见 | +| `409` | 名称冲突、重复连线、DAG 成环或无效 DAG 启用 | +| `412` | `workflow_version` 已过期 | +| `422` | 请求字段、Cron、时区或节点归属不合法 | + diff --git a/deploy/data/workspaces/model-dev/users/admin-zhang/.ipynb_checkpoints/test2-checkpoint.py b/deploy/data/workspaces/model-dev/users/admin-zhang/.ipynb_checkpoints/test2-checkpoint.py new file mode 100644 index 0000000..2dc3e1b --- /dev/null +++ b/deploy/data/workspaces/model-dev/users/admin-zhang/.ipynb_checkpoints/test2-checkpoint.py @@ -0,0 +1,9 @@ +"""模型实验开发平台构建脚本。""" + + +def main() -> None: + print("Hello, Model Platform!") + + +if __name__ == "__main__": + main() diff --git a/deploy/data/workspaces/model-dev/users/admin-zhang/notebooks/.ipynb_checkpoints/test-checkpoint.ipynb b/deploy/data/workspaces/model-dev/users/admin-zhang/notebooks/.ipynb_checkpoints/test-checkpoint.ipynb new file mode 100644 index 0000000..bcf5520 --- /dev/null +++ b/deploy/data/workspaces/model-dev/users/admin-zhang/notebooks/.ipynb_checkpoints/test-checkpoint.ipynb @@ -0,0 +1,43 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "e139eae2", + "metadata": {}, + "source": [ + "# 新建模型实验\\n在这里开始数据探索与模型构建。" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7a78bce5", + "metadata": {}, + "outputs": [], + "source": [ + "print('Hello, Model Platform!')" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.11" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/deploy/data/workspaces/model-dev/users/admin-zhang/notebooks/NotebookDemo.ipynb b/deploy/data/workspaces/model-dev/users/admin-zhang/notebooks/NotebookDemo.ipynb new file mode 100644 index 0000000..7cd3200 --- /dev/null +++ b/deploy/data/workspaces/model-dev/users/admin-zhang/notebooks/NotebookDemo.ipynb @@ -0,0 +1,67 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "initial_id", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": ["hello world\n"] + } + ], + "source": ["print(\"hello world\")"] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "979c21d2489e134d", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": ["hello world12323\n"] + } + ], + "source": ["print(\"hello world12323\")"] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "e068092a-3b51-4399-9c64-c44d58f4973c", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": ["Python 3.12.13\n"] + } + ], + "source": ["!python --version"] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1f512d9e-92f7-423e-9176-40fec30dd79c", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.12.13" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/deploy/data/workspaces/model-dev/users/admin-zhang/notebooks/test.ipynb b/deploy/data/workspaces/model-dev/users/admin-zhang/notebooks/test.ipynb new file mode 100644 index 0000000..4fc54c1 --- /dev/null +++ b/deploy/data/workspaces/model-dev/users/admin-zhang/notebooks/test.ipynb @@ -0,0 +1,59 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "e139eae2", + "metadata": {}, + "source": [ + "# 新建模型实验\\n在这里开始数据探索与模型构建。" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "7a78bce5", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "111\n" + ] + } + ], + "source": [ + "print('111')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "42d2764b-5699-4374-afe9-aecda6d2d1cd", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.11" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/deploy/data/workspaces/model-dev/users/admin-zhang/test2.py b/deploy/data/workspaces/model-dev/users/admin-zhang/test2.py new file mode 100644 index 0000000..2dc3e1b --- /dev/null +++ b/deploy/data/workspaces/model-dev/users/admin-zhang/test2.py @@ -0,0 +1,9 @@ +"""模型实验开发平台构建脚本。""" + + +def main() -> None: + print("Hello, Model Platform!") + + +if __name__ == "__main__": + main() diff --git a/deploy/demo/NotebookDemo.ipynb b/deploy/demo/NotebookDemo.ipynb new file mode 100644 index 0000000..7cd3200 --- /dev/null +++ b/deploy/demo/NotebookDemo.ipynb @@ -0,0 +1,67 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "initial_id", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": ["hello world\n"] + } + ], + "source": ["print(\"hello world\")"] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "979c21d2489e134d", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": ["hello world12323\n"] + } + ], + "source": ["print(\"hello world12323\")"] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "e068092a-3b51-4399-9c64-c44d58f4973c", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": ["Python 3.12.13\n"] + } + ], + "source": ["!python --version"] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1f512d9e-92f7-423e-9176-40fec30dd79c", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.12.13" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docker-compose.legacy.yml b/docker-compose.legacy.yml new file mode 100644 index 0000000..9d27e38 --- /dev/null +++ b/docker-compose.legacy.yml @@ -0,0 +1,60 @@ +version: '3.8' + +services: + web: + image: nginx:alpine + ports: + - "8888:80" + restart: unless-stopped + volumes: + - ./default.conf:/etc/nginx/conf.d/default.conf:ro + depends_on: + - backend + + backend: + build: + context: . + dockerfile: backend/Dockerfile + ports: + - "8004:8000" + environment: + - RUNTIME_BASE_URL=http://runtime:8001 + volumes: + - ./backend:/app/backend:ro + - ./common:/app/common:ro + depends_on: + - runtime + + runtime: + build: + context: . + dockerfile: runtime/Dockerfile + cap_add: + - SYS_ADMIN + devices: + - /dev/fuse:/dev/fuse + security_opt: + - apparmor:unconfined + + ports: + - "8002:8001" + environment: + - PUBLIC_BASE_URL=http://runtime + # --- Rclone 动态环境变量配置 (对应名称 rustfs) --- + - RCLONE_CONFIG_RUSTFS_TYPE=s3 + - RCLONE_CONFIG_RUSTFS_PROVIDER=Other + - RCLONE_CONFIG_RUSTFS_ACCESS_KEY_ID=BdsXeamEnvSDQnk8tRxh + - RCLONE_CONFIG_RUSTFS_SECRET_ACCESS_KEY=mmBVc3RqzbT2VX3ysKGnirYH5kYD3ww3wFtMVvrb + # 替换为你的 RustFS 服务地址(如果是同 docker-compose 网络下的服务,可以直接填服务名:端口) + - RCLONE_CONFIG_RUSTFS_ENDPOINT=http://8.153.151.51:9000 + # 自建 S3 建议强制开启 Path-style 访问 (http://endpoint/bucket) + - RCLONE_CONFIG_RUSTFS_ENV_AUTH=false + - RCLONE_CONFIG_RUSTFS_FORCE_PATH_STYLE=true + - RCLONE_CONFIG_RUSTFS_REGION=other + + # --- Runtime 逻辑环境变量 --- + - REMOTE_BUCKET=rustfs:workspaces + - WORKSPACES_ROOT=/app/workspaces + volumes: + - ./runtime:/app/runtime:ro + - ./common:/app/common:ro \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 13bbd98..16fdec1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,60 +1,202 @@ -version: '3.8' +name: ${COMPOSE_PROJECT_NAME:-model-platform-refactored} + +x-app-environment: &app-environment + DATABASE_URL: mysql+asyncmy://${MYSQL_USER:-model_platform}:${MYSQL_PASSWORD:-model_platform}@mysql:3306/${MYSQL_DATABASE:-model_platform}?charset=utf8mb4 + INTERNAL_SERVICE_TOKEN: ${INTERNAL_SERVICE_TOKEN:-local-internal-token} + JUPYTER_TOKEN: ${JUPYTER_TOKEN:-local-jupyter-token} + WORKSPACE_ROOT: /workspace/workspaces services: - web: - image: nginx:alpine - ports: - - "8888:80" + mysql: + image: mysql:8.0.36 restart: unless-stopped + environment: + MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-model_platform_root} + MYSQL_DATABASE: ${MYSQL_DATABASE:-model_platform} + MYSQL_USER: ${MYSQL_USER:-model_platform} + MYSQL_PASSWORD: ${MYSQL_PASSWORD:-model_platform} + command: + - --character-set-server=utf8mb4 + - --collation-server=utf8mb4_0900_ai_ci + ports: + - "${MYSQL_PORT:-3308}:3306" volumes: - - ./default.conf:/etc/nginx/conf.d/default.conf:ro - depends_on: + - mysql_data:/var/lib/mysql + healthcheck: + test: ["CMD-SHELL", "mysqladmin ping -h 127.0.0.1 -u root -p$$MYSQL_ROOT_PASSWORD --silent"] + interval: 10s + timeout: 5s + retries: 15 + + redis: + image: redis:7.2.5-alpine + restart: unless-stopped + command: ["redis-server", "--appendonly", "yes", "--requirepass", "${REDIS_PASSWORD:-model_platform_redis}"] + ports: + - "${REDIS_PORT:-6380}:6379" + volumes: + - redis_data:/data + healthcheck: + test: ["CMD-SHELL", "redis-cli -a '${REDIS_PASSWORD:-model_platform_redis}' ping | grep PONG"] + interval: 10s + timeout: 5s + retries: 10 + + rustfs: + image: ${RUSTFS_IMAGE:-rustfs/rustfs:latest} + restart: unless-stopped + environment: + RUSTFS_ADDRESS: ":9000" + RUSTFS_CONSOLE_ENABLE: "true" + RUSTFS_CONSOLE_ADDRESS: ":9001" + RUSTFS_ACCESS_KEY: ${RUSTFS_ACCESS_KEY:-modelplatform} + RUSTFS_SECRET_KEY: ${RUSTFS_SECRET_KEY:-modelplatformsecret} + command: ["/data"] + ports: + - "${RUSTFS_API_PORT:-9010}:9000" + - "${RUSTFS_CONSOLE_PORT:-9011}:9001" + volumes: + - rustfs_data:/data + + jupyter: + image: ${JUPYTER_IMAGE:-quay.io/jupyter/base-notebook:2025-12-31} + restart: unless-stopped + environment: + JUPYTER_TOKEN: ${JUPYTER_TOKEN:-local-jupyter-token} + command: + - start-notebook.py + - --ServerApp.base_url=/jupyter/ + - --ServerApp.root_dir=/home/jovyan/work + - --ServerApp.ip=0.0.0.0 + - --ServerApp.allow_remote_access=True + - --IdentityProvider.token=${JUPYTER_TOKEN:-local-jupyter-token} + - --PasswordIdentityProvider.hashed_password= + volumes: + - ./deploy/data/workspaces:/home/jovyan/work + expose: + - "8888" + + migrate: + build: + context: . + dockerfile: backend/Dockerfile + environment: + <<: *app-environment + command: + - uv + - run + - --frozen + - --package - backend + - alembic + - upgrade + - head + depends_on: + mysql: + condition: service_healthy + restart: "no" backend: build: context: . dockerfile: backend/Dockerfile - ports: - - "8000:8000" + restart: unless-stopped environment: - - RUNTIME_BASE_URL=http://runtime:8001 + <<: *app-environment + SERVICE_NAME: backend + READINESS_TARGETS: mysql:3306,redis:6379,rustfs:9000 + RUNTIME_API_URL: http://runtime:8000 + RUSTFS_INTERNAL_ENDPOINT: http://rustfs:9000 + RUSTFS_PUBLIC_ENDPOINT: http://localhost:${RUSTFS_API_PORT:-9010} + RUSTFS_ACCESS_KEY: ${RUSTFS_ACCESS_KEY:-modelplatform} + RUSTFS_SECRET_KEY: ${RUSTFS_SECRET_KEY:-modelplatformsecret} + RUSTFS_DEFAULT_BUCKET: model-platform volumes: - - ./backend:/app/backend:ro - - ./common:/app/common:ro + - ./deploy/data/workspaces:/workspace/workspaces + ports: + - "${BACKEND_PORT:-8010}:8000" depends_on: - - runtime + migrate: + condition: service_completed_successfully + mysql: + condition: service_healthy + redis: + condition: service_healthy + rustfs: + condition: service_started runtime: build: context: . dockerfile: runtime/Dockerfile - cap_add: - - SYS_ADMIN - devices: - - /dev/fuse:/dev/fuse - security_opt: - - apparmor:unconfined - - ports: - - "8001:8001" + restart: unless-stopped environment: - - PUBLIC_BASE_URL=http://runtime - # --- Rclone 动态环境变量配置 (对应名称 rustfs) --- - - RCLONE_CONFIG_RUSTFS_TYPE=s3 - - RCLONE_CONFIG_RUSTFS_PROVIDER=Other - - RCLONE_CONFIG_RUSTFS_ACCESS_KEY_ID=BdsXeamEnvSDQnk8tRxh - - RCLONE_CONFIG_RUSTFS_SECRET_ACCESS_KEY=mmBVc3RqzbT2VX3ysKGnirYH5kYD3ww3wFtMVvrb - # 替换为你的 RustFS 服务地址(如果是同 docker-compose 网络下的服务,可以直接填服务名:端口) - - RCLONE_CONFIG_RUSTFS_ENDPOINT=http://8.153.151.51:9000 - # 自建 S3 建议强制开启 Path-style 访问 (http://endpoint/bucket) - - RCLONE_CONFIG_RUSTFS_ENV_AUTH=false - - RCLONE_CONFIG_RUSTFS_FORCE_PATH_STYLE=true - - RCLONE_CONFIG_RUSTFS_REGION=other + <<: *app-environment + SERVICE_NAME: runtime-manager + READINESS_TARGETS: mysql:3306,redis:6379,jupyter:8888 + REDIS_HOST: redis + REDIS_PORT: "6379" + REDIS_PASSWORD: ${REDIS_PASSWORD:-model_platform_redis} + FILE_LOCK_ENABLED: "false" + JUPYTER_INTERNAL_URL: http://jupyter:8888/jupyter/ + JUPYTER_PROXY_BASE_PATH: /jupyter/ + JUPYTER_TICKET_TTL_SECONDS: "300" + ports: + - "${RUNTIME_PORT:-8012}:8000" + depends_on: + mysql: + condition: service_healthy + redis: + condition: service_healthy + jupyter: + condition: service_started - # --- Runtime 逻辑环境变量 --- - - REMOTE_BUCKET=rustfs:workspaces - - WORKSPACES_ROOT=/app/workspaces + schedule: + build: + context: . + dockerfile: schedule/Dockerfile + restart: unless-stopped + environment: + <<: *app-environment + SERVICE_NAME: schedule-executor + READINESS_TARGETS: mysql:3306,redis:6379,rustfs:9000,backend:8000 + REDIS_HOST: redis + REDIS_PORT: "6379" + REDIS_PASSWORD: ${REDIS_PASSWORD:-model_platform_redis} + RUSTFS_INTERNAL_ENDPOINT: http://rustfs:9000 + RUSTFS_ACCESS_KEY: ${RUSTFS_ACCESS_KEY:-modelplatform} + RUSTFS_SECRET_KEY: ${RUSTFS_SECRET_KEY:-modelplatformsecret} + STORAGE_API_URL: http://backend:8000 volumes: - - ./runtime:/app/runtime:ro - - ./common:/app/common:ro \ No newline at end of file + - ./deploy/data/workspaces:/workspace/workspaces + ports: + - "${SCHEDULE_PORT:-8013}:8000" + depends_on: + mysql: + condition: service_healthy + redis: + condition: service_healthy + backend: + condition: service_started + + gateway: + image: nginx:1.27-alpine + restart: unless-stopped + environment: + INTERNAL_SERVICE_TOKEN: ${INTERNAL_SERVICE_TOKEN:-local-internal-token} + volumes: + - ./nginx/default.conf.template:/etc/nginx/templates/default.conf.template:ro + ports: + - "${GATEWAY_PORT:-8081}:80" + depends_on: + backend: + condition: service_started + runtime: + condition: service_started + jupyter: + condition: service_started + +volumes: + mysql_data: + redis_data: + rustfs_data: diff --git a/migrations/README.md b/migrations/README.md new file mode 100644 index 0000000..48b6823 --- /dev/null +++ b/migrations/README.md @@ -0,0 +1,71 @@ +# Migrations + +MySQL 8 的 Alembic 迁移目录。 + +当前数据库设计基线位于 `4/model_platform_schema.sql`。 + +## V1 基线 + +- Alembic:`1.18.5` +- 基线版本:`20260724_0001` +- 迁移文件:`versions/20260724_0001_v1_schema_baseline.py` +- 业务表:26 张 +- 外键:71 个 +- 索引:94 个 +- `upgrade()`:可以从空 MySQL 8 数据库建立完整 V1 Schema。 +- `downgrade()`:按反向依赖顺序删除 26 张业务表。 + +现有 `model_platform` 数据库原本由 V1 DDL 建立,结构校验一致后已使用 +`alembic stamp head` 接管,没有重复执行建表。 + +## 常用命令 + +所有命令必须通过环境变量传入连接串,仓库中不保存数据库密码: + +```powershell +$env:DATABASE_URL = "mysql+asyncmy://:@:3306/?charset=utf8mb4" +alembic -c alembic.ini current +alembic -c alembic.ini check +alembic -c alembic.ini upgrade head +``` + +迁移发布后不得直接修改旧版本;表结构变化应新增 revision,并评审自动生成的 +数据类型、约束、索引、默认值和回滚顺序。 + +## 第 8 小步验收记录 + +- 执行日期:2026-07-24 +- 目标服务:Docker Compose `mysql` +- 数据库:`model_platform` +- MySQL:8.0.36 +- 空库升级:通过 +- 模型与数据库结构校验:26 张表、71 个外键、差异 0 +- `alembic check`:`No new upgrade operations detected` +- 回滚到 base:通过,剩余业务表 0 +- 临时测试数据库:验收后已删除 +- 现有开发库版本:`20260724_0001 (head)` + +## 第 9 小步数据迁移 + +旧版 `文件1/platform_data/system.json` 已通过 +`data/migrate_system_json.py` 事务化迁移到 MySQL: + +- `roles`:2 行; +- `permissions`:13 行; +- `role_permissions`:21 行; +- `users`:4 行; +- `audit_logs`:26 行。 + +迁移工具默认 dry-run,显式 `--apply` 才写库;重复执行不会重复写入。 + +## 第 10 小步 Workspace 迁移 + +旧版 `文件1/server.py` 中的 `WORKSPACE_DEFINITIONS` 已通过 +`data/migrate_legacy_workspaces.py` 事务化迁移到 MySQL: + +- `workspaces`:2 行; +- `workspace_members`:4 行; +- 已有 `audit_logs.workspace_id`:按用户唯一 Workspace 关系补齐 26 行。 + +迁移工具使用 AST 读取静态常量,不执行旧版服务代码;默认 dry-run,显式 +`--apply` 才写库。重复执行与后端镜像内隔离 dry-run 的变更数均为 0。 diff --git a/migrations/__init__.py b/migrations/__init__.py new file mode 100644 index 0000000..096a364 --- /dev/null +++ b/migrations/__init__.py @@ -0,0 +1 @@ +"""Alembic schema and controlled data migrations.""" diff --git a/migrations/data/README.md b/migrations/data/README.md new file mode 100644 index 0000000..e9a0a48 --- /dev/null +++ b/migrations/data/README.md @@ -0,0 +1,69 @@ +# Data Migrations + +该目录保存从旧版 JSON 状态文件迁移到 MySQL 的一次性工具。 + +数据迁移工具必须满足: + +- 默认只预检,必须显式传入 `--apply` 才能写数据库; +- 单个事务提交,失败时完整回滚; +- 可以安全重复执行,不产生重复数据; +- 输出源文件摘要、源数据数量和实际变更数量; +- 不修改或删除旧版 JSON 源文件。 + +## system.json + +```powershell +$env:DATABASE_URL = "mysql+asyncmy://:@:3306/?charset=utf8mb4" +python -m migrations.data.migrate_system_json --source "/system.json" +python -m migrations.data.migrate_system_json --source "/system.json" --apply +``` + +新迁移用户使用不可登录的占位密码哈希。后续接入认证时,必须通过密码初始化、 +管理员重置或外部身份认证启用登录,不能把旧版无密码账号视为已有凭据。 + +## WORKSPACE_DEFINITIONS + +旧版 Workspace 定义位于 `文件1/server.py` 的 +`WORKSPACE_DEFINITIONS` 常量中。迁移工具通过 Python AST 仅读取这一静态常量, +不执行旧服务代码: + +```powershell +python -m migrations.data.migrate_legacy_workspaces --source "/server.py" +python -m migrations.data.migrate_legacy_workspaces --source "/server.py" --apply +``` + +- 创建者取每个 Workspace 成员中的第一个管理员; +- 成员角色沿用第 9 步迁入的平台注册角色; +- 活跃目录统一为 `file:///workspace/workspaces/{workspace_code}`; +- 制品前缀统一为 `workspaces/{workspace_id}`; +- 旧审计记录仅在用户唯一属于一个 Workspace 时补齐归属。 + +## 第 9 小步执行记录 + +- 执行日期:2026-07-24 +- 源文件:`文件1/platform_data/system.json` +- SHA-256:`ef4ee0e92f4a3679c17f23aff6066688fa9231db4477208e36fcbd343cd446a7` +- 迁移结果:2 个角色、13 个权限、21 条角色权限、4 个用户、26 条审计记录 +- 角色权限:`admin=13`,`developer=8` +- 中文字段:UTF-8 校验通过,问号乱码记录为 0 +- 幂等验证:第二次执行插入和更新均为 0,跳过已有审计记录 26 条 +- 旧版源文件:未修改、未删除 + +## 第 10 小步执行记录 + +- 执行日期:2026-07-24 +- 源文件及常量:`文件1/server.py` / `WORKSPACE_DEFINITIONS` +- SHA-256:`d7ec05f1ab7b2cdeb78f6293de40fbe23fa74b6a7fe4ccb03a9cc64e4cc1f464` +- 迁移结果:2 个 Workspace、4 条成员关系 +- 成员分布:`model-dev` 2 人,`risk-validation` 2 人 +- 审计归属:补齐 26 条,其中 `model-dev` 24 条、`risk-validation` 2 条 +- 中文字段:UTF-8 校验通过,异常记录为 0 +- 幂等验证:第二次执行及镜像内 dry-run 的变更数均为 0 +- 镜像验证:仅挂载旧版 `server.py` 时,迁移工具可独立读取并完成校验 +- 旧版源文件:未修改、未删除 + +## 第 11、12 小步数据处理决定 + +根据实施确认,第 11、12 小步不迁移旧版资源、脚本或稳定版本数据。新实现直接 +使用 MySQL、Workspace 文件目录和 RustFS,从空的 `data_resources`、`scripts` +及 `versions` 表开始运行。功能验收产生的临时对象和数据库记录均已清理。 diff --git a/migrations/data/__init__.py b/migrations/data/__init__.py new file mode 100644 index 0000000..c38c261 --- /dev/null +++ b/migrations/data/__init__.py @@ -0,0 +1 @@ +"""One-time, idempotent data migration tools.""" diff --git a/migrations/data/migrate_legacy_workspaces.py b/migrations/data/migrate_legacy_workspaces.py new file mode 100644 index 0000000..6953d78 --- /dev/null +++ b/migrations/data/migrate_legacy_workspaces.py @@ -0,0 +1,413 @@ +from __future__ import annotations + +import argparse +import ast +import asyncio +import hashlib +import json +import os +from collections import defaultdict +from pathlib import Path +from typing import Any +from urllib.parse import quote + +from pydantic import BaseModel, ConfigDict, TypeAdapter +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from common.db import create_database_engine, create_session_factory +from common.db.models import ( + AuditLogs, + Roles, + Users, + WorkspaceMembers, + Workspaces, +) +from migrations.data.migrate_system_json import ( + deterministic_legacy_ulid, + set_changed, +) + + +class LegacyWorkspace(BaseModel): + model_config = ConfigDict(extra="forbid") + + id: str + name: str + description: str | None = None + userIds: list[str] + portOffset: int + + +WORKSPACE_LIST = TypeAdapter(list[LegacyWorkspace]) + + +def require_unique(values: list[str], label: str) -> None: + duplicates = sorted( + value for value in set(values) if values.count(value) > 1 + ) + if duplicates: + raise ValueError(f"duplicate {label}: {duplicates}") + + +def extract_workspace_definitions( + source_path: Path, +) -> tuple[list[LegacyWorkspace], str]: + raw = source_path.read_bytes() + module = ast.parse( + raw.decode("utf-8-sig"), + filename=str(source_path), + ) + definition: Any | None = None + + for statement in module.body: + if not isinstance(statement, ast.Assign): + continue + if any( + isinstance(target, ast.Name) + and target.id == "WORKSPACE_DEFINITIONS" + for target in statement.targets + ): + definition = ast.literal_eval(statement.value) + break + + if definition is None: + raise ValueError("WORKSPACE_DEFINITIONS was not found") + + workspaces = WORKSPACE_LIST.validate_python(definition) + require_unique([item.id for item in workspaces], "workspace ids") + for workspace in workspaces: + require_unique( + workspace.userIds, + f"members of workspace {workspace.id}", + ) + if not workspace.userIds: + raise ValueError( + f"workspace {workspace.id!r} has no members" + ) + + return workspaces, hashlib.sha256(raw).hexdigest() + + +def new_stats() -> dict[str, int]: + return { + "workspaces_inserted": 0, + "workspaces_updated": 0, + "workspace_members_inserted": 0, + "workspace_members_updated": 0, + "audit_logs_workspace_backfilled": 0, + } + + +async def load_users_and_roles( + session: AsyncSession, +) -> tuple[dict[str, Users], dict[str, str]]: + users = { + item.username: item + for item in ( + await session.scalars(select(Users).order_by(Users.username)) + ).all() + } + roles_by_id = { + item.role_id: item.role_code + for item in ( + await session.scalars(select(Roles).order_by(Roles.role_code)) + ).all() + } + role_codes_by_username = { + username: roles_by_id[user.platform_role_id] + for username, user in users.items() + if user.platform_role_id in roles_by_id + } + return users, role_codes_by_username + + +def validate_members( + workspaces: list[LegacyWorkspace], + users: dict[str, Users], + role_codes_by_username: dict[str, str], +) -> None: + source_members = { + username + for workspace in workspaces + for username in workspace.userIds + } + missing_users = sorted(source_members - set(users)) + if missing_users: + raise ValueError( + f"workspace members were not migrated as users: {missing_users}" + ) + users_without_roles = sorted( + source_members - set(role_codes_by_username) + ) + if users_without_roles: + raise ValueError( + "workspace members have no platform role: " + f"{users_without_roles}" + ) + + +def workspace_creator( + workspace: LegacyWorkspace, + users: dict[str, Users], + role_codes_by_username: dict[str, str], +) -> Users: + admin_username = next( + ( + username + for username in workspace.userIds + if role_codes_by_username[username] == "admin" + ), + workspace.userIds[0], + ) + return users[admin_username] + + +async def migrate_workspaces( + session: AsyncSession, + source: list[LegacyWorkspace], + users: dict[str, Users], + role_codes_by_username: dict[str, str], + stats: dict[str, int], +) -> dict[str, str]: + existing = { + item.workspace_code: item + for item in ( + await session.scalars( + select(Workspaces).order_by(Workspaces.workspace_code) + ) + ).all() + } + workspace_ids: dict[str, str] = {} + + for legacy in source: + workspace = existing.get(legacy.id) + if workspace is None: + workspace_id = deterministic_legacy_ulid( + "workspace", legacy.id + ) + creator = workspace_creator( + legacy, + users, + role_codes_by_username, + ) + workspace = Workspaces( + workspace_id=workspace_id, + workspace_code=legacy.id, + workspace_name=legacy.name, + description=legacy.description, + active_root_uri=( + "file:///workspace/workspaces/" + f"{quote(legacy.id, safe='')}" + ), + quota_bytes=0, + used_bytes=0, + status="active", + created_by=creator.user_id, + artifact_bucket="model-platform", + artifact_prefix=f"workspaces/{workspace_id}", + ) + session.add(workspace) + stats["workspaces_inserted"] += 1 + else: + values = { + "workspace_name": legacy.name, + "description": legacy.description, + "active_root_uri": ( + "file:///workspace/workspaces/" + f"{quote(legacy.id, safe='')}" + ), + "artifact_bucket": "model-platform", + "artifact_prefix": ( + f"workspaces/{workspace.workspace_id}" + ), + } + if set_changed(workspace, values): + stats["workspaces_updated"] += 1 + workspace_ids[legacy.id] = workspace.workspace_id + + return workspace_ids + + +async def migrate_members( + session: AsyncSession, + source: list[LegacyWorkspace], + workspace_ids: dict[str, str], + users: dict[str, Users], + stats: dict[str, int], +) -> None: + existing = { + (item.workspace_id, item.user_id): item + for item in ( + await session.scalars(select(WorkspaceMembers)) + ).all() + } + + for legacy in source: + workspace_id = workspace_ids[legacy.id] + for username in legacy.userIds: + user = users[username] + pair = (workspace_id, user.user_id) + member = existing.get(pair) + values = { + "role_id": user.platform_role_id, + "member_status": "active", + } + if member is None: + member = WorkspaceMembers( + workspace_id=workspace_id, + user_id=user.user_id, + **values, + ) + session.add(member) + existing[pair] = member + stats["workspace_members_inserted"] += 1 + elif set_changed(member, values): + stats["workspace_members_updated"] += 1 + + +async def backfill_audit_workspaces( + session: AsyncSession, + source: list[LegacyWorkspace], + workspace_ids: dict[str, str], + users: dict[str, Users], + stats: dict[str, int], +) -> None: + workspace_codes_by_username: dict[str, set[str]] = defaultdict(set) + for workspace in source: + for username in workspace.userIds: + workspace_codes_by_username[username].add(workspace.id) + + workspace_by_user_id = { + users[username].user_id: workspace_ids[next(iter(codes))] + for username, codes in workspace_codes_by_username.items() + if len(codes) == 1 + } + + audit_logs = ( + await session.scalars( + select(AuditLogs).where(AuditLogs.workspace_id.is_(None)) + ) + ).all() + for audit_log in audit_logs: + if ( + not isinstance(audit_log.detail_json, dict) + or audit_log.detail_json.get("migration_source") + != "platform_data/system.json" + ): + continue + workspace_id = workspace_by_user_id.get( + audit_log.actor_user_id + ) + if workspace_id is not None: + audit_log.workspace_id = workspace_id + stats["audit_logs_workspace_backfilled"] += 1 + + +async def run_migration( + database_url: str, + source: list[LegacyWorkspace], + *, + apply_changes: bool, +) -> dict[str, int]: + engine = create_database_engine(database_url) + factory = create_session_factory(engine) + stats = new_stats() + + try: + async with factory() as session: + try: + users, role_codes_by_username = ( + await load_users_and_roles(session) + ) + validate_members( + source, + users, + role_codes_by_username, + ) + workspace_ids = await migrate_workspaces( + session, + source, + users, + role_codes_by_username, + stats, + ) + await migrate_members( + session, + source, + workspace_ids, + users, + stats, + ) + await backfill_audit_workspaces( + session, + source, + workspace_ids, + users, + stats, + ) + + if apply_changes: + await session.commit() + else: + await session.rollback() + except Exception: + await session.rollback() + raise + finally: + await engine.dispose() + + return stats + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description=( + "Migrate legacy WORKSPACE_DEFINITIONS into MySQL." + ) + ) + parser.add_argument( + "--source", + required=True, + type=Path, + help="Path to the legacy server.py file.", + ) + parser.add_argument( + "--apply", + action="store_true", + help="Commit changes. Without this flag the transaction is rolled back.", + ) + return parser.parse_args() + + +async def async_main() -> None: + args = parse_args() + source_path = args.source.resolve(strict=True) + workspaces, source_sha256 = extract_workspace_definitions(source_path) + stats = await run_migration( + os.environ["DATABASE_URL"], + workspaces, + apply_changes=args.apply, + ) + result = { + "mode": "apply" if args.apply else "dry-run", + "source": str(source_path), + "source_symbol": "WORKSPACE_DEFINITIONS", + "source_sha256": source_sha256, + "source_counts": { + "workspaces": len(workspaces), + "workspace_members": sum( + len(workspace.userIds) for workspace in workspaces + ), + }, + "changes": stats, + } + print(json.dumps(result, ensure_ascii=False, indent=2)) + + +def main() -> None: + asyncio.run(async_main()) + + +if __name__ == "__main__": + main() diff --git a/migrations/data/migrate_system_json.py b/migrations/data/migrate_system_json.py new file mode 100644 index 0000000..8a52322 --- /dev/null +++ b/migrations/data/migrate_system_json.py @@ -0,0 +1,504 @@ +from __future__ import annotations + +import argparse +import asyncio +import hashlib +import json +import os +from datetime import datetime +from pathlib import Path +from typing import Any +from urllib.parse import quote + +from pydantic import BaseModel, ConfigDict, Field +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from common.db import create_database_engine, create_session_factory +from common.db.models import ( + AuditLogs, + Permissions, + RolePermissions, + Roles, + Users, +) + +CROCKFORD_BASE32 = "0123456789ABCDEFGHJKMNPQRSTVWXYZ" +LEGACY_PASSWORD_HASH = "!legacy-account-without-password!" + +PERMISSION_NAMES = { + "dashboard.view": "查看工作台", + "script.build": "构建脚本", + "script.public.manage": "管理公共脚本", + "schedule.own": "管理本人调度", + "schedule.all": "管理全部调度", + "experiment.own": "管理本人实验", + "experiment.all": "管理全部实验", + "resource.personal": "管理个人资源", + "resource.public.upload": "上传公共资源", + "resource.public.manage": "管理公共资源", + "system.view": "查看系统管理", + "system.manage": "管理系统配置", + "audit.view": "查看审计日志", +} + +ACTION_CATALOG = { + "保存调度配置": ("schedule.save", "schedule"), + "删除脚本对象": ("script.delete", "script"), + "删除实验记录": ("experiment.delete", "experiment"), + "删除数据资源": ("data_resource.delete", "data_resource"), + "上传数据资源": ("data_resource.upload", "data_resource"), + "新建脚本对象": ("script.create", "script"), + "修改用户角色": ("user.role.update", "user"), + "运行 Python": ("script.run_python", "script"), + "运行调度": ("schedule.run", "schedule"), +} + + +class LegacyUser(BaseModel): + model_config = ConfigDict(extra="forbid") + + id: str + name: str + role: str + roleKey: str + avatar: str | None = None + + +class LegacyRole(BaseModel): + model_config = ConfigDict(extra="forbid") + + key: str + name: str + description: str | None = None + permissions: list[str] + + +class LegacyAuditLog(BaseModel): + model_config = ConfigDict(extra="forbid") + + id: str + actorId: str + actorName: str + role: str + action: str + target: str + detail: str + status: str + createdAt: str + + +class LegacySystem(BaseModel): + model_config = ConfigDict(extra="forbid") + + users: list[LegacyUser] + roles: list[LegacyRole] + audit_logs: list[LegacyAuditLog] = Field(alias="auditLogs") + + +def deterministic_legacy_ulid(entity_type: str, legacy_key: str) -> str: + """Create a stable ULID-compatible ID with an epoch timestamp prefix.""" + digest = hashlib.sha256( + f"model-platform-v1:{entity_type}:{legacy_key}".encode("utf-8") + ).digest() + value = int.from_bytes(b"\x00" * 6 + digest[:10], byteorder="big") + encoded = ["0"] * 26 + for index in range(25, -1, -1): + encoded[index] = CROCKFORD_BASE32[value & 31] + value >>= 5 + return "".join(encoded) + + +def require_unique(values: list[str], label: str) -> None: + duplicates = sorted( + value for value in set(values) if values.count(value) > 1 + ) + if duplicates: + raise ValueError(f"duplicate {label}: {duplicates}") + + +def validate_source(source: LegacySystem) -> None: + role_codes = [role.key for role in source.roles] + user_codes = [user.id for user in source.users] + audit_ids = [item.id for item in source.audit_logs] + require_unique(role_codes, "role keys") + require_unique(user_codes, "user ids") + require_unique(audit_ids, "audit ids") + + role_code_set = set(role_codes) + unknown_roles = sorted( + user.roleKey + for user in source.users + if user.roleKey not in role_code_set + ) + if unknown_roles: + raise ValueError(f"users reference unknown roles: {unknown_roles}") + + user_code_set = set(user_codes) + unknown_actors = sorted( + item.actorId + for item in source.audit_logs + if item.actorId not in user_code_set + ) + if unknown_actors: + raise ValueError(f"audit logs reference unknown users: {unknown_actors}") + + for role in source.roles: + require_unique(role.permissions, f"permissions of role {role.key}") + for permission_code in role.permissions: + if "." not in permission_code: + raise ValueError( + f"invalid permission code {permission_code!r}" + ) + + +def load_source(path: Path) -> tuple[LegacySystem, str]: + raw = path.read_bytes() + source = LegacySystem.model_validate_json(raw) + validate_source(source) + return source, hashlib.sha256(raw).hexdigest() + + +def set_changed(instance: Any, values: dict[str, Any]) -> bool: + changed = False + for attribute, value in values.items(): + if getattr(instance, attribute) != value: + setattr(instance, attribute, value) + changed = True + return changed + + +def new_stats() -> dict[str, int]: + return { + "roles_inserted": 0, + "roles_updated": 0, + "permissions_inserted": 0, + "permissions_updated": 0, + "role_permissions_inserted": 0, + "users_inserted": 0, + "users_updated": 0, + "audit_logs_inserted": 0, + "audit_logs_skipped": 0, + } + + +async def migrate_roles( + session: AsyncSession, + source: LegacySystem, + stats: dict[str, int], +) -> dict[str, str]: + existing = { + item.role_code: item + for item in ( + await session.scalars(select(Roles).order_by(Roles.role_code)) + ).all() + } + role_ids: dict[str, str] = {} + + for legacy in source.roles: + values = { + "role_name": legacy.name, + "role_scope": "platform", + "is_builtin": 1, + "description": legacy.description, + } + role = existing.get(legacy.key) + if role is None: + role = Roles( + role_id=deterministic_legacy_ulid("role", legacy.key), + role_code=legacy.key, + **values, + ) + session.add(role) + stats["roles_inserted"] += 1 + elif set_changed(role, values): + stats["roles_updated"] += 1 + role_ids[legacy.key] = role.role_id + + return role_ids + + +async def migrate_permissions( + session: AsyncSession, + source: LegacySystem, + stats: dict[str, int], +) -> dict[str, str]: + permission_codes = sorted( + { + permission_code + for role in source.roles + for permission_code in role.permissions + } + ) + existing = { + item.permission_code: item + for item in ( + await session.scalars( + select(Permissions).order_by(Permissions.permission_code) + ) + ).all() + } + permission_ids: dict[str, str] = {} + + for permission_code in permission_codes: + values = { + "permission_name": PERMISSION_NAMES.get( + permission_code, permission_code + ), + "module_code": permission_code.split(".", 1)[0], + "description": f"由旧版 system.json 迁移:{permission_code}", + } + permission = existing.get(permission_code) + if permission is None: + permission = Permissions( + permission_id=deterministic_legacy_ulid( + "permission", permission_code + ), + permission_code=permission_code, + **values, + ) + session.add(permission) + stats["permissions_inserted"] += 1 + elif set_changed(permission, values): + stats["permissions_updated"] += 1 + permission_ids[permission_code] = permission.permission_id + + return permission_ids + + +async def migrate_role_permissions( + session: AsyncSession, + source: LegacySystem, + role_ids: dict[str, str], + permission_ids: dict[str, str], + stats: dict[str, int], +) -> None: + existing = set( + ( + await session.execute( + select( + RolePermissions.role_id, + RolePermissions.permission_id, + ) + ) + ).all() + ) + + for role in source.roles: + for permission_code in role.permissions: + pair = ( + role_ids[role.key], + permission_ids[permission_code], + ) + if pair not in existing: + session.add( + RolePermissions( + role_id=pair[0], + permission_id=pair[1], + ) + ) + existing.add(pair) + stats["role_permissions_inserted"] += 1 + + +async def migrate_users( + session: AsyncSession, + source: LegacySystem, + role_ids: dict[str, str], + stats: dict[str, int], +) -> dict[str, str]: + existing = { + item.username: item + for item in ( + await session.scalars(select(Users).order_by(Users.username)) + ).all() + } + user_ids: dict[str, str] = {} + + for legacy in source.users: + values = { + "display_name": legacy.name, + "platform_role_id": role_ids[legacy.roleKey], + "avatar_uri": ( + f"initial://{quote(legacy.avatar)}" + if legacy.avatar + else None + ), + } + user = existing.get(legacy.id) + if user is None: + user = Users( + user_id=deterministic_legacy_ulid("user", legacy.id), + username=legacy.id, + password_hash=LEGACY_PASSWORD_HASH, + status="active", + **values, + ) + session.add(user) + stats["users_inserted"] += 1 + elif set_changed(user, values): + stats["users_updated"] += 1 + user_ids[legacy.id] = user.user_id + + return user_ids + + +def audit_catalog(action: str) -> tuple[str, str]: + known = ACTION_CATALOG.get(action) + if known is not None: + return known + digest = hashlib.sha256(action.encode("utf-8")).hexdigest()[:16] + return (f"legacy.action.{digest}", "legacy") + + +async def migrate_audit_logs( + session: AsyncSession, + source: LegacySystem, + user_ids: dict[str, str], + stats: dict[str, int], +) -> None: + existing_payloads = ( + await session.scalars(select(AuditLogs.detail_json)) + ).all() + existing_legacy_ids = { + payload.get("legacy_id") + for payload in existing_payloads + if isinstance(payload, dict) and payload.get("legacy_id") + } + + for legacy in sorted(source.audit_logs, key=lambda item: item.createdAt): + if legacy.id in existing_legacy_ids: + stats["audit_logs_skipped"] += 1 + continue + + action_code, target_type = audit_catalog(legacy.action) + session.add( + AuditLogs( + actor_user_id=user_ids[legacy.actorId], + action_code=action_code, + target_type=target_type, + target_id=legacy.target[:128] or None, + operation_status=( + "success" if legacy.status == "成功" else "failed" + ), + created_at=datetime.strptime( + legacy.createdAt, "%Y-%m-%d %H:%M:%S" + ), + detail_json={ + "legacy_id": legacy.id, + "legacy_actor_name": legacy.actorName, + "legacy_role": legacy.role, + "legacy_action": legacy.action, + "legacy_target": legacy.target, + "legacy_detail": legacy.detail, + "legacy_status": legacy.status, + "migration_source": "platform_data/system.json", + }, + ) + ) + existing_legacy_ids.add(legacy.id) + stats["audit_logs_inserted"] += 1 + + +async def run_migration( + database_url: str, + source: LegacySystem, + *, + apply_changes: bool, +) -> dict[str, int]: + engine = create_database_engine(database_url) + factory = create_session_factory(engine) + stats = new_stats() + + try: + async with factory() as session: + try: + role_ids = await migrate_roles(session, source, stats) + permission_ids = await migrate_permissions( + session, source, stats + ) + await migrate_role_permissions( + session, + source, + role_ids, + permission_ids, + stats, + ) + user_ids = await migrate_users( + session, source, role_ids, stats + ) + await migrate_audit_logs( + session, source, user_ids, stats + ) + + if apply_changes: + await session.commit() + else: + await session.rollback() + except Exception: + await session.rollback() + raise + finally: + await engine.dispose() + + return stats + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="Migrate legacy system.json records into MySQL." + ) + parser.add_argument( + "--source", + required=True, + type=Path, + help="Path to the legacy platform_data/system.json file.", + ) + parser.add_argument( + "--apply", + action="store_true", + help="Commit changes. Without this flag the transaction is rolled back.", + ) + return parser.parse_args() + + +async def async_main() -> None: + args = parse_args() + source_path = args.source.resolve(strict=True) + source, source_sha256 = load_source(source_path) + database_url = os.environ["DATABASE_URL"] + stats = await run_migration( + database_url, + source, + apply_changes=args.apply, + ) + result = { + "mode": "apply" if args.apply else "dry-run", + "source": str(source_path), + "source_sha256": source_sha256, + "source_counts": { + "roles": len(source.roles), + "permissions": len( + { + permission + for role in source.roles + for permission in role.permissions + } + ), + "role_permissions": sum( + len(role.permissions) for role in source.roles + ), + "users": len(source.users), + "audit_logs": len(source.audit_logs), + }, + "changes": stats, + } + print(json.dumps(result, ensure_ascii=False, indent=2)) + + +def main() -> None: + asyncio.run(async_main()) + + +if __name__ == "__main__": + main() diff --git a/migrations/env.py b/migrations/env.py new file mode 100644 index 0000000..4b06419 --- /dev/null +++ b/migrations/env.py @@ -0,0 +1,127 @@ +from __future__ import annotations + +import asyncio +import os +from decimal import Decimal, InvalidOperation +from logging.config import fileConfig +from typing import Any + +from alembic import context +from sqlalchemy import Connection, pool +from sqlalchemy.ext.asyncio import async_engine_from_config + +from common.db import Base + +config = context.config + +if config.config_file_name is not None: + fileConfig(config.config_file_name) + +target_metadata = Base.metadata + + +def canonical_default(value: Any) -> tuple[str, Any] | None: + """Normalize harmless MySQL quoting and numeric formatting differences.""" + if value is None: + return None + + text_value = str(value).strip() + while ( + len(text_value) >= 2 + and text_value.startswith("(") + and text_value.endswith(")") + ): + text_value = text_value[1:-1].strip() + if ( + len(text_value) >= 2 + and text_value[0] == text_value[-1] + and text_value[0] in {"'", '"'} + ): + text_value = text_value[1:-1] + + try: + return ("number", Decimal(text_value).normalize()) + except InvalidOperation: + return ("text", text_value.casefold()) + + +def compare_server_default( + migration_context: Any, + inspected_column: Any, + metadata_column: Any, + inspected_default: str | None, + metadata_default: Any, + rendered_metadata_default: str | None, +) -> bool | None: + """Suppress formatting-only differences and defer real changes to Alembic.""" + del migration_context, inspected_column, metadata_column, metadata_default + if canonical_default(inspected_default) == canonical_default( + rendered_metadata_default + ): + return False + return None + + +def database_url() -> str: + """Return the runtime database URL without storing credentials in the repo.""" + try: + return os.environ["DATABASE_URL"] + except KeyError as exc: + raise RuntimeError( + "DATABASE_URL is required for Alembic commands" + ) from exc + + +def configure_context(*, connection: Connection | None = None) -> None: + options = { + "target_metadata": target_metadata, + "compare_type": True, + "compare_server_default": compare_server_default, + } + if connection is None: + context.configure( + url=database_url(), + literal_binds=True, + dialect_opts={"paramstyle": "named"}, + **options, + ) + else: + context.configure(connection=connection, **options) + + +def run_migrations_offline() -> None: + configure_context() + with context.begin_transaction(): + context.run_migrations() + + +def do_run_migrations(connection: Connection) -> None: + configure_context(connection=connection) + with context.begin_transaction(): + context.run_migrations() + + +async def run_async_migrations() -> None: + section = config.get_section(config.config_ini_section, {}) + section["sqlalchemy.url"] = database_url() + connectable = async_engine_from_config( + section, + prefix="sqlalchemy.", + poolclass=pool.NullPool, + ) + + try: + async with connectable.connect() as connection: + await connection.run_sync(do_run_migrations) + finally: + await connectable.dispose() + + +def run_migrations_online() -> None: + asyncio.run(run_async_migrations()) + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/migrations/script.py.mako b/migrations/script.py.mako new file mode 100644 index 0000000..9bb4c4e --- /dev/null +++ b/migrations/script.py.mako @@ -0,0 +1,28 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} +""" + +from collections.abc import Sequence + +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision: str = ${repr(up_revision)} +down_revision: str | Sequence[str] | None = ${repr(down_revision)} +branch_labels: str | Sequence[str] | None = ${repr(branch_labels)} +depends_on: str | Sequence[str] | None = ${repr(depends_on)} + + +def upgrade() -> None: + """Upgrade schema.""" + ${upgrades if upgrades else "pass"} + + +def downgrade() -> None: + """Downgrade schema.""" + ${downgrades if downgrades else "pass"} diff --git a/migrations/versions/20260724_0001_v1_schema_baseline.py b/migrations/versions/20260724_0001_v1_schema_baseline.py new file mode 100644 index 0000000..11a37fa --- /dev/null +++ b/migrations/versions/20260724_0001_v1_schema_baseline.py @@ -0,0 +1,652 @@ +"""v1 schema baseline + +Revision ID: 20260724_0001 +Revises: +Create Date: 2026-07-24 05:57:13.620909 +""" + +from collections.abc import Sequence + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import mysql + +# revision identifiers, used by Alembic. +revision: str = '20260724_0001' +down_revision: str | Sequence[str] | None = None +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('consumer_inbox', + sa.Column('consumer_name', sa.String(length=128), nullable=False), + sa.Column('event_id', sa.CHAR(length=26), nullable=False), + sa.Column('process_status', sa.String(length=16), server_default=sa.text("'processing'"), nullable=False, comment='processing/succeeded/failed'), + sa.Column('created_at', mysql.DATETIME(fsp=3), server_default=sa.text('CURRENT_TIMESTAMP(3)'), nullable=False), + sa.Column('message_id', sa.String(length=128), nullable=True, comment='Redis Stream message ID'), + sa.Column('processed_at', mysql.DATETIME(fsp=3), nullable=True), + sa.Column('error_message', sa.String(length=2000), nullable=True), + sa.PrimaryKeyConstraint('consumer_name', 'event_id'), + comment='消费者幂等 Inbox,防止 Stream 重投导致重复执行' + ) + op.create_index('idx_consumer_inbox_status', 'consumer_inbox', ['consumer_name', 'process_status', 'created_at'], unique=False) + op.create_table('outbox_events', + sa.Column('event_id', sa.CHAR(length=26), nullable=False), + sa.Column('aggregate_type', sa.String(length=64), nullable=False), + sa.Column('aggregate_id', sa.String(length=128), nullable=False), + sa.Column('event_type', sa.String(length=128), nullable=False), + sa.Column('schema_version', mysql.SMALLINT(), server_default=sa.text('1'), nullable=False), + sa.Column('payload_json', sa.JSON(), nullable=False), + sa.Column('event_status', sa.String(length=16), server_default=sa.text("'pending'"), nullable=False, comment='pending/published/failed'), + sa.Column('available_at', mysql.DATETIME(fsp=3), server_default=sa.text('CURRENT_TIMESTAMP(3)'), nullable=False), + sa.Column('retry_count', mysql.INTEGER(), server_default=sa.text('0'), nullable=False), + sa.Column('created_at', mysql.DATETIME(fsp=3), server_default=sa.text('CURRENT_TIMESTAMP(3)'), nullable=False), + sa.Column('trace_id', sa.String(length=64), nullable=True), + sa.Column('idempotency_key', sa.String(length=128), nullable=True), + sa.Column('published_at', mysql.DATETIME(fsp=3), nullable=True), + sa.Column('last_error', sa.String(length=2000), nullable=True), + sa.PrimaryKeyConstraint('event_id'), + comment='事务 Outbox;提交后发布到 Redis Streams' + ) + op.create_index('idx_outbox_aggregate', 'outbox_events', ['aggregate_type', 'aggregate_id', 'created_at'], unique=False) + op.create_index('idx_outbox_idempotency', 'outbox_events', ['idempotency_key'], unique=False) + op.create_index('idx_outbox_pending', 'outbox_events', ['event_status', 'available_at', 'created_at'], unique=False) + op.create_table('permissions', + sa.Column('permission_id', sa.CHAR(length=26), nullable=False), + sa.Column('permission_code', sa.String(length=128), nullable=False), + sa.Column('permission_name', sa.String(length=100), nullable=False), + sa.Column('module_code', sa.String(length=64), nullable=False), + sa.Column('created_at', mysql.DATETIME(fsp=3), server_default=sa.text('CURRENT_TIMESTAMP(3)'), nullable=False), + sa.Column('description', sa.String(length=500), nullable=True), + sa.PrimaryKeyConstraint('permission_id'), + comment='权限点' + ) + op.create_index('idx_permissions_module', 'permissions', ['module_code'], unique=False) + op.create_index('uk_permissions_code', 'permissions', ['permission_code'], unique=True) + op.create_table('roles', + sa.Column('role_id', sa.CHAR(length=26), nullable=False), + sa.Column('role_code', sa.String(length=64), nullable=False), + sa.Column('role_name', sa.String(length=100), nullable=False), + sa.Column('role_scope', sa.String(length=16), nullable=False, comment='platform/workspace'), + sa.Column('is_builtin', mysql.TINYINT(display_width=1), server_default=sa.text('0'), nullable=False), + sa.Column('created_at', mysql.DATETIME(fsp=3), server_default=sa.text('CURRENT_TIMESTAMP(3)'), nullable=False), + sa.Column('updated_at', mysql.DATETIME(fsp=3), server_default=sa.text('CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)'), nullable=False), + sa.Column('description', sa.String(length=500), nullable=True), + sa.PrimaryKeyConstraint('role_id'), + comment='角色' + ) + op.create_index('uk_roles_code', 'roles', ['role_code'], unique=True) + op.create_table('role_permissions', + sa.Column('role_id', sa.CHAR(length=26), nullable=False), + sa.Column('permission_id', sa.CHAR(length=26), nullable=False), + sa.Column('created_at', mysql.DATETIME(fsp=3), server_default=sa.text('CURRENT_TIMESTAMP(3)'), nullable=False), + sa.ForeignKeyConstraint(['permission_id'], ['permissions.permission_id'], name='fk_role_permissions_permission', ondelete='CASCADE'), + sa.ForeignKeyConstraint(['role_id'], ['roles.role_id'], name='fk_role_permissions_role', ondelete='CASCADE'), + sa.PrimaryKeyConstraint('role_id', 'permission_id'), + comment='角色权限' + ) + op.create_index('fk_role_permissions_permission', 'role_permissions', ['permission_id'], unique=False) + op.create_table('users', + sa.Column('user_id', sa.CHAR(length=26), nullable=False), + sa.Column('username', sa.String(length=64), nullable=False), + sa.Column('display_name', sa.String(length=100), nullable=False), + sa.Column('password_hash', sa.String(length=255), nullable=False), + sa.Column('status', sa.String(length=16), server_default=sa.text("'active'"), nullable=False, comment='active/disabled/locked'), + sa.Column('created_at', mysql.DATETIME(fsp=3), server_default=sa.text('CURRENT_TIMESTAMP(3)'), nullable=False), + sa.Column('updated_at', mysql.DATETIME(fsp=3), server_default=sa.text('CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)'), nullable=False), + sa.Column('email', sa.String(length=255), nullable=True), + sa.Column('platform_role_id', sa.CHAR(length=26), nullable=True), + sa.Column('avatar_uri', sa.String(length=1000), nullable=True), + sa.Column('last_login_at', mysql.DATETIME(fsp=3), nullable=True), + sa.Column('deleted_at', mysql.DATETIME(fsp=3), nullable=True), + sa.ForeignKeyConstraint(['platform_role_id'], ['roles.role_id'], name='fk_users_platform_role', ondelete='SET NULL'), + sa.PrimaryKeyConstraint('user_id'), + comment='平台用户' + ) + op.create_index('fk_users_platform_role', 'users', ['platform_role_id'], unique=False) + op.create_index('idx_users_status', 'users', ['status'], unique=False) + op.create_index('uk_users_email', 'users', ['email'], unique=True) + op.create_index('uk_users_username', 'users', ['username'], unique=True) + op.create_table('workspaces', + sa.Column('workspace_id', sa.CHAR(length=26), nullable=False), + sa.Column('workspace_code', sa.String(length=64), nullable=False), + sa.Column('workspace_name', sa.String(length=150), nullable=False), + sa.Column('active_root_uri', sa.String(length=1500), nullable=False, comment='活动工作区,建议 NFS/PVC/file URI'), + sa.Column('quota_bytes', mysql.BIGINT(), server_default=sa.text('0'), nullable=False, comment='0 表示不限额'), + sa.Column('used_bytes', mysql.BIGINT(), server_default=sa.text('0'), nullable=False), + sa.Column('status', sa.String(length=24), server_default=sa.text("'active'"), nullable=False, comment='creating/active/suspended/deleting/deleted'), + sa.Column('created_by', sa.CHAR(length=26), nullable=False), + sa.Column('created_at', mysql.DATETIME(fsp=3), server_default=sa.text('CURRENT_TIMESTAMP(3)'), nullable=False), + sa.Column('updated_at', mysql.DATETIME(fsp=3), server_default=sa.text('CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)'), nullable=False), + sa.Column('description', sa.String(length=1000), nullable=True), + sa.Column('artifact_bucket', sa.String(length=128), nullable=True, comment='RustFS bucket'), + sa.Column('artifact_prefix', sa.String(length=512), nullable=True, comment='RustFS object key prefix'), + sa.Column('deleted_at', mysql.DATETIME(fsp=3), nullable=True), + sa.ForeignKeyConstraint(['created_by'], ['users.user_id'], name='fk_workspaces_created_by', ondelete='RESTRICT'), + sa.PrimaryKeyConstraint('workspace_id'), + comment='Workspace' + ) + op.create_index('fk_workspaces_created_by', 'workspaces', ['created_by'], unique=False) + op.create_index('idx_workspaces_status', 'workspaces', ['status'], unique=False) + op.create_index('uk_workspaces_code', 'workspaces', ['workspace_code'], unique=True) + op.create_table('audit_logs', + sa.Column('audit_id', mysql.BIGINT(), nullable=False), + sa.Column('action_code', sa.String(length=128), nullable=False), + sa.Column('target_type', sa.String(length=64), nullable=False), + sa.Column('operation_status', sa.String(length=16), server_default=sa.text("'success'"), nullable=False), + sa.Column('created_at', mysql.DATETIME(fsp=3), server_default=sa.text('CURRENT_TIMESTAMP(3)'), nullable=False), + sa.Column('workspace_id', sa.CHAR(length=26), nullable=True), + sa.Column('actor_user_id', sa.CHAR(length=26), nullable=True), + sa.Column('target_id', sa.String(length=128), nullable=True), + sa.Column('client_ip', sa.String(length=45), nullable=True), + sa.Column('user_agent', sa.String(length=1000), nullable=True), + sa.Column('detail_json', sa.JSON(), nullable=True), + sa.ForeignKeyConstraint(['actor_user_id'], ['users.user_id'], name='fk_audit_actor', ondelete='SET NULL'), + sa.ForeignKeyConstraint(['workspace_id'], ['workspaces.workspace_id'], name='fk_audit_workspace', ondelete='SET NULL'), + sa.PrimaryKeyConstraint('audit_id'), + comment='操作审计日志' + ) + op.create_index('idx_audit_action_time', 'audit_logs', ['action_code', 'created_at'], unique=False) + op.create_index('idx_audit_actor_time', 'audit_logs', ['actor_user_id', 'created_at'], unique=False) + op.create_index('idx_audit_workspace_time', 'audit_logs', ['workspace_id', 'created_at'], unique=False) + op.create_table('runtime_instances', + sa.Column('runtime_id', sa.CHAR(length=26), nullable=False), + sa.Column('workspace_id', sa.CHAR(length=26), nullable=False), + sa.Column('runtime_type', sa.String(length=24), server_default=sa.text("'jupyter'"), nullable=False), + sa.Column('runtime_provider', sa.String(length=24), nullable=False, comment='process/docker/kubernetes'), + sa.Column('proxy_base_path', sa.String(length=512), nullable=False), + sa.Column('desired_state', sa.String(length=16), server_default=sa.text("'running'"), nullable=False), + sa.Column('actual_state', sa.String(length=24), server_default=sa.text("'provisioning'"), nullable=False, comment='provisioning/starting/running/unhealthy/stopping/stopped/failed'), + sa.Column('state_version', mysql.INTEGER(), server_default=sa.text('0'), nullable=False, comment='乐观锁版本'), + sa.Column('started_by', sa.CHAR(length=26), nullable=False), + sa.Column('created_at', mysql.DATETIME(fsp=3), server_default=sa.text('CURRENT_TIMESTAMP(3)'), nullable=False), + sa.Column('updated_at', mysql.DATETIME(fsp=3), server_default=sa.text('CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)'), nullable=False), + sa.Column('owner_user_id', sa.CHAR(length=26), nullable=True, comment='为空表示 Workspace 级 Runtime'), + sa.Column('runtime_ref', sa.String(length=255), nullable=True, comment='PID/container ID/pod UID'), + sa.Column('host_node', sa.String(length=255), nullable=True), + sa.Column('internal_url', sa.String(length=1000), nullable=True), + sa.Column('started_at', mysql.DATETIME(fsp=3), nullable=True), + sa.Column('last_heartbeat_at', mysql.DATETIME(fsp=3), nullable=True), + sa.Column('lease_expires_at', mysql.DATETIME(fsp=3), nullable=True), + sa.Column('stopped_at', mysql.DATETIME(fsp=3), nullable=True), + sa.Column('error_message', sa.Text(), nullable=True), + sa.ForeignKeyConstraint(['owner_user_id'], ['users.user_id'], name='fk_runtime_owner', ondelete='SET NULL'), + sa.ForeignKeyConstraint(['started_by'], ['users.user_id'], name='fk_runtime_started_by', ondelete='RESTRICT'), + sa.ForeignKeyConstraint(['workspace_id'], ['workspaces.workspace_id'], name='fk_runtime_workspace', ondelete='RESTRICT'), + sa.PrimaryKeyConstraint('runtime_id'), + comment='Jupyter/未来 VS Code、OpenCode Runtime 实例' + ) + op.create_index('fk_runtime_started_by', 'runtime_instances', ['started_by'], unique=False) + op.create_index('idx_runtime_lease', 'runtime_instances', ['actual_state', 'lease_expires_at'], unique=False) + op.create_index('idx_runtime_owner_state', 'runtime_instances', ['owner_user_id', 'actual_state'], unique=False) + op.create_index('idx_runtime_workspace_state', 'runtime_instances', ['workspace_id', 'runtime_type', 'actual_state'], unique=False) + op.create_table('schedules', + sa.Column('schedule_id', sa.CHAR(length=26), nullable=False), + sa.Column('workspace_id', sa.CHAR(length=26), nullable=False), + sa.Column('schedule_name', sa.String(length=255), nullable=False), + sa.Column('trigger_type', sa.String(length=16), server_default=sa.text("'cron'"), nullable=False, comment='manual/cron/api'), + sa.Column('timezone', sa.String(length=64), server_default=sa.text("'Asia/Shanghai'"), nullable=False), + sa.Column('enabled', mysql.TINYINT(display_width=1), server_default=sa.text('0'), nullable=False), + sa.Column('workflow_version', mysql.INTEGER(), server_default=sa.text('1'), nullable=False), + sa.Column('max_concurrency', mysql.INTEGER(), server_default=sa.text('1'), nullable=False), + sa.Column('failure_policy', sa.String(length=24), server_default=sa.text("'stop'"), nullable=False, comment='stop/continue'), + sa.Column('created_by', sa.CHAR(length=26), nullable=False), + sa.Column('updated_by', sa.CHAR(length=26), nullable=False), + sa.Column('created_at', mysql.DATETIME(fsp=3), server_default=sa.text('CURRENT_TIMESTAMP(3)'), nullable=False), + sa.Column('updated_at', mysql.DATETIME(fsp=3), server_default=sa.text('CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)'), nullable=False), + sa.Column('description', sa.String(length=1000), nullable=True), + sa.Column('cron_expression', sa.String(length=128), nullable=True), + sa.Column('last_run_at', mysql.DATETIME(fsp=3), nullable=True), + sa.Column('next_run_at', mysql.DATETIME(fsp=3), nullable=True), + sa.Column('deleted_at', mysql.DATETIME(fsp=3), nullable=True), + sa.ForeignKeyConstraint(['created_by'], ['users.user_id'], name='fk_schedules_created_by', ondelete='RESTRICT'), + sa.ForeignKeyConstraint(['updated_by'], ['users.user_id'], name='fk_schedules_updated_by', ondelete='RESTRICT'), + sa.ForeignKeyConstraint(['workspace_id'], ['workspaces.workspace_id'], name='fk_schedules_workspace', ondelete='RESTRICT'), + sa.PrimaryKeyConstraint('schedule_id'), + comment='调度方案' + ) + op.create_index('fk_schedules_created_by', 'schedules', ['created_by'], unique=False) + op.create_index('fk_schedules_updated_by', 'schedules', ['updated_by'], unique=False) + op.create_index('idx_schedules_due', 'schedules', ['enabled', 'next_run_at'], unique=False) + op.create_index('idx_schedules_workspace', 'schedules', ['workspace_id', 'enabled', 'updated_at'], unique=False) + op.create_table('storage_objects', + sa.Column('storage_object_id', sa.CHAR(length=26), nullable=False), + sa.Column('workspace_id', sa.CHAR(length=26), nullable=False), + sa.Column('object_type', sa.String(length=16), nullable=False, comment='file/directory'), + sa.Column('usage_type', sa.String(length=32), nullable=False, comment='working_copy/public_script/data_resource/version_artifact/snapshot/run_log/run_result'), + sa.Column('storage_backend', sa.String(length=16), nullable=False, comment='workspace_fs/rustfs'), + sa.Column('storage_uri', sa.String(length=1500), nullable=False), + sa.Column('file_name', sa.String(length=255), nullable=False), + sa.Column('size_bytes', mysql.BIGINT(), server_default=sa.text('0'), nullable=False), + sa.Column('visibility', sa.String(length=16), server_default=sa.text("'private'"), nullable=False, comment='private/workspace/public'), + sa.Column('is_immutable', mysql.TINYINT(display_width=1), server_default=sa.text('0'), nullable=False), + sa.Column('object_status', sa.String(length=24), server_default=sa.text("'available'"), nullable=False, comment='uploading/available/deleting/deleted/failed'), + sa.Column('created_by', sa.CHAR(length=26), nullable=False), + sa.Column('created_at', mysql.DATETIME(fsp=3), server_default=sa.text('CURRENT_TIMESTAMP(3)'), nullable=False), + sa.Column('updated_at', mysql.DATETIME(fsp=3), server_default=sa.text('CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)'), nullable=False), + sa.Column('owner_user_id', sa.CHAR(length=26), nullable=True), + sa.Column('parent_object_id', sa.CHAR(length=26), nullable=True), + sa.Column('relative_path', sa.String(length=1024), nullable=True, comment='Workspace 相对路径'), + sa.Column('path_hash', sa.BINARY(length=32), nullable=True, comment='SHA-256(relative_path),由应用写入'), + sa.Column('bucket_name', sa.String(length=128), nullable=True), + sa.Column('object_key', sa.String(length=1024), nullable=True), + sa.Column('object_key_hash', sa.BINARY(length=32), nullable=True, comment='SHA-256(object_key),由应用写入'), + sa.Column('file_extension', sa.String(length=32), nullable=True), + sa.Column('mime_type', sa.String(length=255), nullable=True), + sa.Column('content_hash', sa.CHAR(length=64), nullable=True, comment='SHA-256 hex'), + sa.Column('object_etag', sa.String(length=255), nullable=True), + sa.Column('deleted_at', mysql.DATETIME(fsp=3), nullable=True), + sa.ForeignKeyConstraint(['created_by'], ['users.user_id'], name='fk_storage_created_by', ondelete='RESTRICT'), + sa.ForeignKeyConstraint(['owner_user_id'], ['users.user_id'], name='fk_storage_owner', ondelete='SET NULL'), + sa.ForeignKeyConstraint(['parent_object_id'], ['storage_objects.storage_object_id'], name='fk_storage_parent', ondelete='SET NULL'), + sa.ForeignKeyConstraint(['workspace_id'], ['workspaces.workspace_id'], name='fk_storage_workspace', ondelete='RESTRICT'), + sa.PrimaryKeyConstraint('storage_object_id'), + comment='Workspace 文件和 RustFS 对象的统一元数据' + ) + op.create_index('fk_storage_created_by', 'storage_objects', ['created_by'], unique=False) + op.create_index('idx_storage_content_hash', 'storage_objects', ['content_hash'], unique=False) + op.create_index('idx_storage_owner', 'storage_objects', ['owner_user_id', 'object_status'], unique=False) + op.create_index('idx_storage_parent', 'storage_objects', ['parent_object_id'], unique=False) + op.create_index('idx_storage_workspace_usage', 'storage_objects', ['workspace_id', 'usage_type', 'object_status'], unique=False) + op.create_index('uk_storage_bucket_key', 'storage_objects', ['storage_backend', 'bucket_name', 'object_key_hash'], unique=True) + op.create_index('uk_storage_workspace_path', 'storage_objects', ['workspace_id', 'storage_backend', 'path_hash'], unique=True) + op.create_table('workspace_members', + sa.Column('workspace_id', sa.CHAR(length=26), nullable=False), + sa.Column('user_id', sa.CHAR(length=26), nullable=False), + sa.Column('role_id', sa.CHAR(length=26), nullable=False), + sa.Column('member_status', sa.String(length=16), server_default=sa.text("'active'"), nullable=False), + sa.Column('joined_at', mysql.DATETIME(fsp=3), server_default=sa.text('CURRENT_TIMESTAMP(3)'), nullable=False), + sa.Column('updated_at', mysql.DATETIME(fsp=3), server_default=sa.text('CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)'), nullable=False), + sa.ForeignKeyConstraint(['role_id'], ['roles.role_id'], name='fk_workspace_members_role', ondelete='RESTRICT'), + sa.ForeignKeyConstraint(['user_id'], ['users.user_id'], name='fk_workspace_members_user', ondelete='RESTRICT'), + sa.ForeignKeyConstraint(['workspace_id'], ['workspaces.workspace_id'], name='fk_workspace_members_workspace', ondelete='CASCADE'), + sa.PrimaryKeyConstraint('workspace_id', 'user_id'), + comment='Workspace 成员与角色' + ) + op.create_index('idx_workspace_members_role', 'workspace_members', ['role_id'], unique=False) + op.create_index('idx_workspace_members_user', 'workspace_members', ['user_id', 'member_status'], unique=False) + op.create_table('data_resources', + sa.Column('resource_id', sa.CHAR(length=26), nullable=False), + sa.Column('workspace_id', sa.CHAR(length=26), nullable=False), + sa.Column('storage_object_id', sa.CHAR(length=26), nullable=False), + sa.Column('owner_user_id', sa.CHAR(length=26), nullable=False), + sa.Column('resource_name', sa.String(length=255), nullable=False), + sa.Column('visibility', sa.String(length=16), server_default=sa.text("'private'"), nullable=False), + sa.Column('status', sa.String(length=16), server_default=sa.text("'active'"), nullable=False), + sa.Column('created_at', mysql.DATETIME(fsp=3), server_default=sa.text('CURRENT_TIMESTAMP(3)'), nullable=False), + sa.Column('updated_at', mysql.DATETIME(fsp=3), server_default=sa.text('CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)'), nullable=False), + sa.Column('description', sa.String(length=1000), nullable=True), + sa.Column('schema_json', sa.JSON(), nullable=True, comment='字段结构、行数等可选元数据'), + sa.Column('deleted_at', mysql.DATETIME(fsp=3), nullable=True), + sa.ForeignKeyConstraint(['owner_user_id'], ['users.user_id'], name='fk_data_resources_owner', ondelete='RESTRICT'), + sa.ForeignKeyConstraint(['storage_object_id'], ['storage_objects.storage_object_id'], name='fk_data_resources_object', ondelete='RESTRICT'), + sa.ForeignKeyConstraint(['workspace_id'], ['workspaces.workspace_id'], name='fk_data_resources_workspace', ondelete='RESTRICT'), + sa.PrimaryKeyConstraint('resource_id'), + comment='数据资源' + ) + op.create_index('idx_data_resources_owner', 'data_resources', ['owner_user_id', 'status'], unique=False) + op.create_index('idx_data_resources_workspace', 'data_resources', ['workspace_id', 'visibility', 'status'], unique=False) + op.create_index('uk_data_resources_object', 'data_resources', ['storage_object_id'], unique=True) + op.create_table('edit_sessions', + sa.Column('edit_session_id', sa.CHAR(length=26), nullable=False), + sa.Column('workspace_id', sa.CHAR(length=26), nullable=False), + sa.Column('storage_object_id', sa.CHAR(length=26), nullable=False), + sa.Column('user_id', sa.CHAR(length=26), nullable=False), + sa.Column('redis_lock_key', sa.String(length=512), nullable=False), + sa.Column('lock_token_hash', sa.BINARY(length=32), nullable=False, comment='不保存原始 token'), + sa.Column('session_status', sa.String(length=16), server_default=sa.text("'active'"), nullable=False, comment='active/closed/expired/failed'), + sa.Column('started_at', mysql.DATETIME(fsp=3), server_default=sa.text('CURRENT_TIMESTAMP(3)'), nullable=False), + sa.Column('last_heartbeat_at', mysql.DATETIME(fsp=3), server_default=sa.text('CURRENT_TIMESTAMP(3)'), nullable=False), + sa.Column('expires_at', mysql.DATETIME(fsp=3), nullable=False), + sa.Column('runtime_id', sa.CHAR(length=26), nullable=True), + sa.Column('jupyter_session_id', sa.String(length=255), nullable=True), + sa.Column('ended_at', mysql.DATETIME(fsp=3), nullable=True), + sa.Column('end_reason', sa.String(length=64), nullable=True), + sa.ForeignKeyConstraint(['runtime_id'], ['runtime_instances.runtime_id'], name='fk_edit_sessions_runtime', ondelete='SET NULL'), + sa.ForeignKeyConstraint(['storage_object_id'], ['storage_objects.storage_object_id'], name='fk_edit_sessions_object', ondelete='RESTRICT'), + sa.ForeignKeyConstraint(['user_id'], ['users.user_id'], name='fk_edit_sessions_user', ondelete='RESTRICT'), + sa.ForeignKeyConstraint(['workspace_id'], ['workspaces.workspace_id'], name='fk_edit_sessions_workspace', ondelete='RESTRICT'), + sa.PrimaryKeyConstraint('edit_session_id'), + comment='编辑会话审计;实时锁状态以 Redis 为准' + ) + op.create_index('fk_edit_sessions_workspace', 'edit_sessions', ['workspace_id'], unique=False) + op.create_index('idx_edit_sessions_object', 'edit_sessions', ['storage_object_id', 'session_status', 'expires_at'], unique=False) + op.create_index('idx_edit_sessions_runtime', 'edit_sessions', ['runtime_id', 'session_status'], unique=False) + op.create_index('idx_edit_sessions_user', 'edit_sessions', ['user_id', 'session_status'], unique=False) + op.create_table('schedule_runs', + sa.Column('run_id', sa.CHAR(length=26), nullable=False), + sa.Column('schedule_id', sa.CHAR(length=26), nullable=False), + sa.Column('workspace_id', sa.CHAR(length=26), nullable=False), + sa.Column('workflow_version', mysql.INTEGER(), nullable=False), + sa.Column('trigger_type', sa.String(length=16), nullable=False, comment='manual/cron/api/retry'), + sa.Column('idempotency_key', sa.String(length=128), nullable=False), + sa.Column('run_status', sa.String(length=24), server_default=sa.text("'queued'"), nullable=False, comment='queued/running/succeeded/failed/cancelled/timed_out'), + sa.Column('state_version', mysql.INTEGER(), server_default=sa.text('0'), nullable=False, comment='乐观锁版本'), + sa.Column('schedule_snapshot', sa.JSON(), nullable=False, comment='执行时 DAG 快照'), + sa.Column('queued_at', mysql.DATETIME(fsp=3), server_default=sa.text('CURRENT_TIMESTAMP(3)'), nullable=False), + sa.Column('created_at', mysql.DATETIME(fsp=3), server_default=sa.text('CURRENT_TIMESTAMP(3)'), nullable=False), + sa.Column('triggered_by', sa.CHAR(length=26), nullable=True), + sa.Column('started_at', mysql.DATETIME(fsp=3), nullable=True), + sa.Column('finished_at', mysql.DATETIME(fsp=3), nullable=True), + sa.Column('duration_ms', mysql.BIGINT(), nullable=True), + sa.Column('error_code', sa.String(length=64), nullable=True), + sa.Column('error_message', sa.Text(), nullable=True), + sa.Column('logs_object_id', sa.CHAR(length=26), nullable=True), + sa.Column('result_object_id', sa.CHAR(length=26), nullable=True), + sa.ForeignKeyConstraint(['logs_object_id'], ['storage_objects.storage_object_id'], name='fk_schedule_runs_logs', ondelete='SET NULL'), + sa.ForeignKeyConstraint(['result_object_id'], ['storage_objects.storage_object_id'], name='fk_schedule_runs_result', ondelete='SET NULL'), + sa.ForeignKeyConstraint(['schedule_id'], ['schedules.schedule_id'], name='fk_schedule_runs_schedule', ondelete='RESTRICT'), + sa.ForeignKeyConstraint(['triggered_by'], ['users.user_id'], name='fk_schedule_runs_user', ondelete='SET NULL'), + sa.ForeignKeyConstraint(['workspace_id'], ['workspaces.workspace_id'], name='fk_schedule_runs_workspace', ondelete='RESTRICT'), + sa.PrimaryKeyConstraint('run_id'), + comment='调度运行' + ) + op.create_index('fk_schedule_runs_logs', 'schedule_runs', ['logs_object_id'], unique=False) + op.create_index('fk_schedule_runs_result', 'schedule_runs', ['result_object_id'], unique=False) + op.create_index('fk_schedule_runs_user', 'schedule_runs', ['triggered_by'], unique=False) + op.create_index('idx_schedule_runs_schedule', 'schedule_runs', ['schedule_id', 'created_at'], unique=False) + op.create_index('idx_schedule_runs_status', 'schedule_runs', ['run_status', 'queued_at'], unique=False) + op.create_index('idx_schedule_runs_workspace_status', 'schedule_runs', ['workspace_id', 'run_status', 'queued_at'], unique=False) + op.create_index('uk_schedule_runs_idempotency', 'schedule_runs', ['idempotency_key'], unique=True) + op.create_table('scripts', + sa.Column('script_id', sa.CHAR(length=26), nullable=False), + sa.Column('workspace_id', sa.CHAR(length=26), nullable=False), + sa.Column('current_object_id', sa.CHAR(length=26), nullable=False, comment='当前工作副本'), + sa.Column('owner_user_id', sa.CHAR(length=26), nullable=False), + sa.Column('script_name', sa.String(length=255), nullable=False), + sa.Column('script_type', sa.String(length=16), nullable=False, comment='python/notebook'), + sa.Column('visibility', sa.String(length=16), server_default=sa.text("'private'"), nullable=False), + sa.Column('status', sa.String(length=16), server_default=sa.text("'active'"), nullable=False), + sa.Column('created_at', mysql.DATETIME(fsp=3), server_default=sa.text('CURRENT_TIMESTAMP(3)'), nullable=False), + sa.Column('updated_at', mysql.DATETIME(fsp=3), server_default=sa.text('CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)'), nullable=False), + sa.Column('deleted_at', mysql.DATETIME(fsp=3), nullable=True), + sa.ForeignKeyConstraint(['current_object_id'], ['storage_objects.storage_object_id'], name='fk_scripts_current_object', ondelete='RESTRICT'), + sa.ForeignKeyConstraint(['owner_user_id'], ['users.user_id'], name='fk_scripts_owner', ondelete='RESTRICT'), + sa.ForeignKeyConstraint(['workspace_id'], ['workspaces.workspace_id'], name='fk_scripts_workspace', ondelete='RESTRICT'), + sa.PrimaryKeyConstraint('script_id'), + comment='可执行 Python/Notebook 脚本' + ) + op.create_index('idx_scripts_owner', 'scripts', ['owner_user_id', 'status'], unique=False) + op.create_index('idx_scripts_workspace', 'scripts', ['workspace_id', 'script_type', 'visibility', 'status'], unique=False) + op.create_index('uk_scripts_current_object', 'scripts', ['current_object_id'], unique=True) + op.create_table('upload_sessions', + sa.Column('upload_id', sa.CHAR(length=26), nullable=False), + sa.Column('workspace_id', sa.CHAR(length=26), nullable=False), + sa.Column('user_id', sa.CHAR(length=26), nullable=False), + sa.Column('idempotency_key', sa.String(length=128), nullable=False), + sa.Column('bucket_name', sa.String(length=128), nullable=False), + sa.Column('object_key', sa.String(length=1024), nullable=False), + sa.Column('object_key_hash', sa.BINARY(length=32), nullable=False), + sa.Column('upload_status', sa.String(length=24), server_default=sa.text("'created'"), nullable=False, comment='created/uploading/completed/expired/aborted/failed'), + sa.Column('expires_at', mysql.DATETIME(fsp=3), nullable=False), + sa.Column('created_at', mysql.DATETIME(fsp=3), server_default=sa.text('CURRENT_TIMESTAMP(3)'), nullable=False), + sa.Column('updated_at', mysql.DATETIME(fsp=3), server_default=sa.text('CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)'), nullable=False), + sa.Column('multipart_upload_id', sa.String(length=255), nullable=True), + sa.Column('expected_size_bytes', mysql.BIGINT(), nullable=True), + sa.Column('expected_hash', sa.CHAR(length=64), nullable=True), + sa.Column('content_type', sa.String(length=255), nullable=True), + sa.Column('storage_object_id', sa.CHAR(length=26), nullable=True), + sa.Column('completed_at', mysql.DATETIME(fsp=3), nullable=True), + sa.ForeignKeyConstraint(['storage_object_id'], ['storage_objects.storage_object_id'], name='fk_upload_sessions_storage_object', ondelete='SET NULL'), + sa.ForeignKeyConstraint(['user_id'], ['users.user_id'], name='fk_upload_sessions_user', ondelete='RESTRICT'), + sa.ForeignKeyConstraint(['workspace_id'], ['workspaces.workspace_id'], name='fk_upload_sessions_workspace', ondelete='RESTRICT'), + sa.PrimaryKeyConstraint('upload_id'), + comment='RustFS 预签名上传会话;URL 本身不持久化' + ) + op.create_index('fk_upload_sessions_storage_object', 'upload_sessions', ['storage_object_id'], unique=False) + op.create_index('fk_upload_sessions_user', 'upload_sessions', ['user_id'], unique=False) + op.create_index('idx_upload_sessions_expiry', 'upload_sessions', ['upload_status', 'expires_at'], unique=False) + op.create_index('idx_upload_sessions_object_key', 'upload_sessions', ['bucket_name', 'object_key_hash'], unique=False) + op.create_index('idx_upload_sessions_workspace', 'upload_sessions', ['workspace_id', 'user_id', 'created_at'], unique=False) + op.create_index('uk_upload_sessions_idempotency', 'upload_sessions', ['idempotency_key'], unique=True) + op.create_table('workspace_operations', + sa.Column('operation_id', sa.CHAR(length=26), nullable=False), + sa.Column('workspace_id', sa.CHAR(length=26), nullable=False), + sa.Column('operation_type', sa.String(length=24), nullable=False, comment='open/close/mount/unmount/start/stop/restart/recycle'), + sa.Column('operation_status', sa.String(length=24), server_default=sa.text("'pending'"), nullable=False, comment='pending/running/succeeded/failed/cancelled'), + sa.Column('state_version', mysql.INTEGER(), server_default=sa.text('0'), nullable=False, comment='乐观锁版本'), + sa.Column('requested_by', sa.CHAR(length=26), nullable=False), + sa.Column('created_at', mysql.DATETIME(fsp=3), server_default=sa.text('CURRENT_TIMESTAMP(3)'), nullable=False), + sa.Column('runtime_id', sa.CHAR(length=26), nullable=True), + sa.Column('request_id', sa.String(length=128), nullable=True), + sa.Column('started_at', mysql.DATETIME(fsp=3), nullable=True), + sa.Column('finished_at', mysql.DATETIME(fsp=3), nullable=True), + sa.Column('error_code', sa.String(length=64), nullable=True), + sa.Column('error_message', sa.Text(), nullable=True), + sa.ForeignKeyConstraint(['requested_by'], ['users.user_id'], name='fk_workspace_operations_user', ondelete='RESTRICT'), + sa.ForeignKeyConstraint(['runtime_id'], ['runtime_instances.runtime_id'], name='fk_workspace_operations_runtime', ondelete='SET NULL'), + sa.ForeignKeyConstraint(['workspace_id'], ['workspaces.workspace_id'], name='fk_workspace_operations_workspace', ondelete='RESTRICT'), + sa.PrimaryKeyConstraint('operation_id'), + comment='无状态 Backend 的 Workspace/Jupyter 异步操作记录' + ) + op.create_index('fk_workspace_operations_user', 'workspace_operations', ['requested_by'], unique=False) + op.create_index('idx_workspace_operations_runtime', 'workspace_operations', ['runtime_id', 'created_at'], unique=False) + op.create_index('idx_workspace_operations_workspace', 'workspace_operations', ['workspace_id', 'operation_status', 'created_at'], unique=False) + op.create_index('uk_workspace_operations_request', 'workspace_operations', ['request_id'], unique=True) + op.create_table('notebook_snapshots', + sa.Column('snapshot_id', sa.CHAR(length=26), nullable=False), + sa.Column('workspace_id', sa.CHAR(length=26), nullable=False), + sa.Column('script_id', sa.CHAR(length=26), nullable=False), + sa.Column('source_object_id', sa.CHAR(length=26), nullable=False), + sa.Column('artifact_object_id', sa.CHAR(length=26), nullable=False), + sa.Column('snapshot_name', sa.String(length=255), nullable=False), + sa.Column('content_hash', sa.CHAR(length=64), nullable=False), + sa.Column('outputs_stripped', mysql.TINYINT(display_width=1), server_default=sa.text('1'), nullable=False), + sa.Column('created_by', sa.CHAR(length=26), nullable=False), + sa.Column('created_at', mysql.DATETIME(fsp=3), server_default=sa.text('CURRENT_TIMESTAMP(3)'), nullable=False), + sa.Column('description', sa.String(length=1000), nullable=True), + sa.ForeignKeyConstraint(['artifact_object_id'], ['storage_objects.storage_object_id'], name='fk_snapshots_artifact_object', ondelete='RESTRICT'), + sa.ForeignKeyConstraint(['created_by'], ['users.user_id'], name='fk_snapshots_created_by', ondelete='RESTRICT'), + sa.ForeignKeyConstraint(['script_id'], ['scripts.script_id'], name='fk_snapshots_script', ondelete='RESTRICT'), + sa.ForeignKeyConstraint(['source_object_id'], ['storage_objects.storage_object_id'], name='fk_snapshots_source_object', ondelete='RESTRICT'), + sa.ForeignKeyConstraint(['workspace_id'], ['workspaces.workspace_id'], name='fk_snapshots_workspace', ondelete='RESTRICT'), + sa.PrimaryKeyConstraint('snapshot_id'), + comment='Notebook 开发快照,append-only' + ) + op.create_index('fk_snapshots_created_by', 'notebook_snapshots', ['created_by'], unique=False) + op.create_index('fk_snapshots_source_object', 'notebook_snapshots', ['source_object_id'], unique=False) + op.create_index('idx_snapshots_workspace_created', 'notebook_snapshots', ['workspace_id', 'created_at'], unique=False) + op.create_index('uk_snapshots_artifact', 'notebook_snapshots', ['artifact_object_id'], unique=True) + op.create_index('uk_snapshots_script_hash', 'notebook_snapshots', ['script_id', 'content_hash'], unique=True) + op.create_table('versions', + sa.Column('versions_id', sa.CHAR(length=26), nullable=False, comment='稳定版本唯一 ID'), + sa.Column('workspace_id', sa.CHAR(length=26), nullable=False), + sa.Column('script_id', sa.CHAR(length=26), nullable=False), + sa.Column('source_object_id', sa.CHAR(length=26), nullable=False, comment='发布时的源对象'), + sa.Column('artifact_object_id', sa.CHAR(length=26), nullable=False, comment='RustFS 不可变版本制品'), + sa.Column('version_no', mysql.INTEGER(), nullable=False), + sa.Column('version_label', sa.String(length=32), nullable=False, comment='例如 v1.0'), + sa.Column('source_path', sa.String(length=1024), nullable=False, comment='发布时路径快照'), + sa.Column('artifact_path', sa.String(length=1500), nullable=False), + sa.Column('content_hash', sa.CHAR(length=64), nullable=False), + sa.Column('file_size_bytes', mysql.BIGINT(), server_default=sa.text('0'), nullable=False), + sa.Column('visibility', sa.String(length=16), server_default=sa.text("'private'"), nullable=False), + sa.Column('created_by', sa.CHAR(length=26), nullable=False), + sa.Column('created_at', mysql.DATETIME(fsp=3), server_default=sa.text('CURRENT_TIMESTAMP(3)'), nullable=False), + sa.Column('release_note', sa.String(length=1000), nullable=True), + sa.ForeignKeyConstraint(['artifact_object_id'], ['storage_objects.storage_object_id'], name='fk_versions_artifact_object', ondelete='RESTRICT'), + sa.ForeignKeyConstraint(['created_by'], ['users.user_id'], name='fk_versions_created_by', ondelete='RESTRICT'), + sa.ForeignKeyConstraint(['script_id'], ['scripts.script_id'], name='fk_versions_script', ondelete='RESTRICT'), + sa.ForeignKeyConstraint(['source_object_id'], ['storage_objects.storage_object_id'], name='fk_versions_source_object', ondelete='RESTRICT'), + sa.ForeignKeyConstraint(['workspace_id'], ['workspaces.workspace_id'], name='fk_versions_workspace', ondelete='RESTRICT'), + sa.PrimaryKeyConstraint('versions_id'), + comment='不可变稳定版本;调度节点必须引用 versions_id' + ) + op.create_index('fk_versions_source_object', 'versions', ['source_object_id'], unique=False) + op.create_index('idx_versions_creator', 'versions', ['created_by', 'created_at'], unique=False) + op.create_index('idx_versions_workspace_created', 'versions', ['workspace_id', 'created_at'], unique=False) + op.create_index('uk_versions_artifact', 'versions', ['artifact_object_id'], unique=True) + op.create_index('uk_versions_script_hash', 'versions', ['script_id', 'content_hash'], unique=True) + op.create_index('uk_versions_script_no', 'versions', ['script_id', 'version_no'], unique=True) + op.create_table('experiments', + sa.Column('experiment_id', sa.CHAR(length=26), nullable=False), + sa.Column('workspace_id', sa.CHAR(length=26), nullable=False), + sa.Column('owner_user_id', sa.CHAR(length=26), nullable=False), + sa.Column('experiment_name', sa.String(length=255), nullable=False), + sa.Column('source_type', sa.String(length=24), nullable=False, comment='python/notebook/schedule/rerun'), + sa.Column('experiment_status', sa.String(length=24), server_default=sa.text("'queued'"), nullable=False), + sa.Column('state_version', mysql.INTEGER(), server_default=sa.text('0'), nullable=False, comment='乐观锁版本'), + sa.Column('created_at', mysql.DATETIME(fsp=3), server_default=sa.text('CURRENT_TIMESTAMP(3)'), nullable=False), + sa.Column('script_id', sa.CHAR(length=26), nullable=True), + sa.Column('versions_id', sa.CHAR(length=26), nullable=True, comment='工作副本运行时可为空'), + sa.Column('schedule_run_id', sa.CHAR(length=26), nullable=True), + sa.Column('parent_experiment_id', sa.CHAR(length=26), nullable=True), + sa.Column('parameters_json', sa.JSON(), nullable=True), + sa.Column('environment_json', sa.JSON(), nullable=True), + sa.Column('result_summary', sa.String(length=2000), nullable=True), + sa.Column('logs_object_id', sa.CHAR(length=26), nullable=True), + sa.Column('result_object_id', sa.CHAR(length=26), nullable=True), + sa.Column('started_at', mysql.DATETIME(fsp=3), nullable=True), + sa.Column('finished_at', mysql.DATETIME(fsp=3), nullable=True), + sa.Column('duration_ms', mysql.BIGINT(), nullable=True), + sa.Column('deleted_at', mysql.DATETIME(fsp=3), nullable=True), + sa.ForeignKeyConstraint(['logs_object_id'], ['storage_objects.storage_object_id'], name='fk_experiments_logs', ondelete='SET NULL'), + sa.ForeignKeyConstraint(['owner_user_id'], ['users.user_id'], name='fk_experiments_owner', ondelete='RESTRICT'), + sa.ForeignKeyConstraint(['parent_experiment_id'], ['experiments.experiment_id'], name='fk_experiments_parent', ondelete='SET NULL'), + sa.ForeignKeyConstraint(['result_object_id'], ['storage_objects.storage_object_id'], name='fk_experiments_result', ondelete='SET NULL'), + sa.ForeignKeyConstraint(['schedule_run_id'], ['schedule_runs.run_id'], name='fk_experiments_schedule_run', ondelete='SET NULL'), + sa.ForeignKeyConstraint(['script_id'], ['scripts.script_id'], name='fk_experiments_script', ondelete='SET NULL'), + sa.ForeignKeyConstraint(['versions_id'], ['versions.versions_id'], name='fk_experiments_version', ondelete='SET NULL'), + sa.ForeignKeyConstraint(['workspace_id'], ['workspaces.workspace_id'], name='fk_experiments_workspace', ondelete='RESTRICT'), + sa.PrimaryKeyConstraint('experiment_id'), + comment='实验记录' + ) + op.create_index('fk_experiments_logs', 'experiments', ['logs_object_id'], unique=False) + op.create_index('fk_experiments_parent', 'experiments', ['parent_experiment_id'], unique=False) + op.create_index('fk_experiments_result', 'experiments', ['result_object_id'], unique=False) + op.create_index('fk_experiments_script', 'experiments', ['script_id'], unique=False) + op.create_index('idx_experiments_owner', 'experiments', ['owner_user_id', 'created_at'], unique=False) + op.create_index('idx_experiments_schedule_run', 'experiments', ['schedule_run_id'], unique=False) + op.create_index('idx_experiments_version', 'experiments', ['versions_id'], unique=False) + op.create_index('idx_experiments_workspace', 'experiments', ['workspace_id', 'experiment_status', 'created_at'], unique=False) + op.create_table('schedule_nodes', + sa.Column('node_id', sa.CHAR(length=26), nullable=False), + sa.Column('schedule_id', sa.CHAR(length=26), nullable=False), + sa.Column('node_key', sa.String(length=64), nullable=False, comment='画布内稳定标识'), + sa.Column('node_name', sa.String(length=255), nullable=False), + sa.Column('versions_id', sa.CHAR(length=26), nullable=False), + sa.Column('timeout_seconds', mysql.INTEGER(), server_default=sa.text('600'), nullable=False), + sa.Column('retry_count', mysql.INTEGER(), server_default=sa.text('0'), nullable=False), + sa.Column('retry_interval_sec', mysql.INTEGER(), server_default=sa.text('5'), nullable=False), + sa.Column('position_x', sa.DECIMAL(precision=10, scale=2), server_default=sa.text('0.00'), nullable=False), + sa.Column('position_y', sa.DECIMAL(precision=10, scale=2), server_default=sa.text('0.00'), nullable=False), + sa.Column('created_at', mysql.DATETIME(fsp=3), server_default=sa.text('CURRENT_TIMESTAMP(3)'), nullable=False), + sa.Column('updated_at', mysql.DATETIME(fsp=3), server_default=sa.text('CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)'), nullable=False), + sa.Column('arguments_json', sa.JSON(), nullable=True), + sa.Column('env_refs_json', sa.JSON(), nullable=True, comment='只存密钥引用,不存明文密钥'), + sa.ForeignKeyConstraint(['schedule_id'], ['schedules.schedule_id'], name='fk_schedule_nodes_schedule', ondelete='CASCADE'), + sa.ForeignKeyConstraint(['versions_id'], ['versions.versions_id'], name='fk_schedule_nodes_version', ondelete='RESTRICT'), + sa.PrimaryKeyConstraint('node_id'), + comment='DAG 节点,必须引用稳定版本' + ) + op.create_index('idx_schedule_nodes_version', 'schedule_nodes', ['versions_id'], unique=False) + op.create_index('uk_schedule_nodes_key', 'schedule_nodes', ['schedule_id', 'node_key'], unique=True) + op.create_table('experiment_metrics', + sa.Column('metric_id', mysql.BIGINT(), nullable=False), + sa.Column('experiment_id', sa.CHAR(length=26), nullable=False), + sa.Column('metric_name', sa.String(length=128), nullable=False), + sa.Column('recorded_at', mysql.DATETIME(fsp=3), server_default=sa.text('CURRENT_TIMESTAMP(3)'), nullable=False), + sa.Column('metric_value', sa.Double(asdecimal=True), nullable=True), + sa.Column('metric_text', sa.String(length=1000), nullable=True), + sa.Column('step_no', sa.BigInteger(), nullable=True), + sa.ForeignKeyConstraint(['experiment_id'], ['experiments.experiment_id'], name='fk_experiment_metrics_experiment', ondelete='CASCADE'), + sa.PrimaryKeyConstraint('metric_id'), + comment='实验指标,支持筛选和曲线' + ) + op.create_index('idx_experiment_metrics_lookup', 'experiment_metrics', ['experiment_id', 'metric_name', 'step_no'], unique=False) + op.create_table('experiment_resources', + sa.Column('experiment_id', sa.CHAR(length=26), nullable=False), + sa.Column('resource_id', sa.CHAR(length=26), nullable=False), + sa.Column('resource_role', sa.String(length=16), server_default=sa.text("'input'"), nullable=False, comment='input/output'), + sa.Column('created_at', mysql.DATETIME(fsp=3), server_default=sa.text('CURRENT_TIMESTAMP(3)'), nullable=False), + sa.ForeignKeyConstraint(['experiment_id'], ['experiments.experiment_id'], name='fk_experiment_resources_experiment', ondelete='CASCADE'), + sa.ForeignKeyConstraint(['resource_id'], ['data_resources.resource_id'], name='fk_experiment_resources_resource', ondelete='RESTRICT'), + sa.PrimaryKeyConstraint('experiment_id', 'resource_id', 'resource_role'), + comment='实验与数据资源' + ) + op.create_index('fk_experiment_resources_resource', 'experiment_resources', ['resource_id'], unique=False) + op.create_table('schedule_edges', + sa.Column('edge_id', sa.CHAR(length=26), nullable=False), + sa.Column('schedule_id', sa.CHAR(length=26), nullable=False), + sa.Column('source_node_id', sa.CHAR(length=26), nullable=False), + sa.Column('target_node_id', sa.CHAR(length=26), nullable=False), + sa.Column('created_at', mysql.DATETIME(fsp=3), server_default=sa.text('CURRENT_TIMESTAMP(3)'), nullable=False), + sa.Column('condition_expr', sa.String(length=1000), nullable=True), + sa.ForeignKeyConstraint(['schedule_id'], ['schedules.schedule_id'], name='fk_schedule_edges_schedule', ondelete='CASCADE'), + sa.ForeignKeyConstraint(['source_node_id'], ['schedule_nodes.node_id'], name='fk_schedule_edges_source', ondelete='CASCADE'), + sa.ForeignKeyConstraint(['target_node_id'], ['schedule_nodes.node_id'], name='fk_schedule_edges_target', ondelete='CASCADE'), + sa.PrimaryKeyConstraint('edge_id'), + comment='DAG 有向边' + ) + op.create_index('fk_schedule_edges_source', 'schedule_edges', ['source_node_id'], unique=False) + op.create_index('idx_schedule_edges_target', 'schedule_edges', ['target_node_id'], unique=False) + op.create_index('uk_schedule_edges_pair', 'schedule_edges', ['schedule_id', 'source_node_id', 'target_node_id'], unique=True) + op.create_table('schedule_node_runs', + sa.Column('node_run_id', sa.CHAR(length=26), nullable=False), + sa.Column('run_id', sa.CHAR(length=26), nullable=False), + sa.Column('node_id', sa.CHAR(length=26), nullable=False), + sa.Column('versions_id', sa.CHAR(length=26), nullable=False), + sa.Column('attempt_no', mysql.INTEGER(), server_default=sa.text('1'), nullable=False), + sa.Column('node_status', sa.String(length=24), server_default=sa.text("'queued'"), nullable=False, comment='queued/running/succeeded/failed/skipped/cancelled/timed_out'), + sa.Column('state_version', mysql.INTEGER(), server_default=sa.text('0'), nullable=False, comment='乐观锁版本'), + sa.Column('created_at', mysql.DATETIME(fsp=3), server_default=sa.text('CURRENT_TIMESTAMP(3)'), nullable=False), + sa.Column('started_at', mysql.DATETIME(fsp=3), nullable=True), + sa.Column('finished_at', mysql.DATETIME(fsp=3), nullable=True), + sa.Column('duration_ms', mysql.BIGINT(), nullable=True), + sa.Column('exit_code', sa.Integer(), nullable=True), + sa.Column('message', sa.String(length=2000), nullable=True), + sa.Column('metrics_json', sa.JSON(), nullable=True), + sa.Column('logs_object_id', sa.CHAR(length=26), nullable=True), + sa.Column('result_object_id', sa.CHAR(length=26), nullable=True), + sa.ForeignKeyConstraint(['logs_object_id'], ['storage_objects.storage_object_id'], name='fk_node_runs_logs', ondelete='SET NULL'), + sa.ForeignKeyConstraint(['node_id'], ['schedule_nodes.node_id'], name='fk_node_runs_node', ondelete='RESTRICT'), + sa.ForeignKeyConstraint(['result_object_id'], ['storage_objects.storage_object_id'], name='fk_node_runs_result', ondelete='SET NULL'), + sa.ForeignKeyConstraint(['run_id'], ['schedule_runs.run_id'], name='fk_node_runs_run', ondelete='CASCADE'), + sa.ForeignKeyConstraint(['versions_id'], ['versions.versions_id'], name='fk_node_runs_version', ondelete='RESTRICT'), + sa.PrimaryKeyConstraint('node_run_id'), + comment='调度节点运行与重试' + ) + op.create_index('fk_node_runs_logs', 'schedule_node_runs', ['logs_object_id'], unique=False) + op.create_index('fk_node_runs_node', 'schedule_node_runs', ['node_id'], unique=False) + op.create_index('fk_node_runs_result', 'schedule_node_runs', ['result_object_id'], unique=False) + op.create_index('idx_node_runs_status', 'schedule_node_runs', ['run_id', 'node_status'], unique=False) + op.create_index('idx_node_runs_version', 'schedule_node_runs', ['versions_id'], unique=False) + op.create_index('uk_node_runs_attempt', 'schedule_node_runs', ['run_id', 'node_id', 'attempt_no'], unique=True) + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # MySQL removes indexes with their table. Dropping FK-supporting indexes + # explicitly first raises error 1553, so tables are removed in reverse + # dependency order and MySQL performs the index cleanup. + op.drop_table('schedule_node_runs') + op.drop_table('schedule_edges') + op.drop_table('experiment_resources') + op.drop_table('experiment_metrics') + op.drop_table('schedule_nodes') + op.drop_table('experiments') + op.drop_table('versions') + op.drop_table('notebook_snapshots') + op.drop_table('workspace_operations') + op.drop_table('upload_sessions') + op.drop_table('scripts') + op.drop_table('schedule_runs') + op.drop_table('edit_sessions') + op.drop_table('data_resources') + op.drop_table('workspace_members') + op.drop_table('storage_objects') + op.drop_table('schedules') + op.drop_table('runtime_instances') + op.drop_table('audit_logs') + op.drop_table('workspaces') + op.drop_table('users') + op.drop_table('role_permissions') + op.drop_table('roles') + op.drop_table('permissions') + op.drop_table('outbox_events') + op.drop_table('consumer_inbox') + # ### end Alembic commands ### diff --git a/migrations/versions/20260728_0002_demo_workspaces_users.py b/migrations/versions/20260728_0002_demo_workspaces_users.py new file mode 100644 index 0000000..ec15f48 --- /dev/null +++ b/migrations/versions/20260728_0002_demo_workspaces_users.py @@ -0,0 +1,112 @@ +"""demo workspaces and users + +Revision ID: 20260728_0002 +Revises: 20260724_0001 +Create Date: 2026-07-28 +""" + +from collections.abc import Sequence + +from alembic import op + + +revision: str = "20260728_0002" +down_revision: str | Sequence[str] | None = "20260724_0001" +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None + + +ADMIN_ROLE = "0000000000HNQN4KM476QNKW1C" +DEVELOPER_ROLE = "00000000005RCQ4GPGBK3WZYMM" +MODEL_WORKSPACE = "00000000000BM630VT9ARVFZPC" +RISK_WORKSPACE = "0000000000AE0NC0V5T424KK86" +ZHANG = "0000000000RF6FG1SDBXG59S13" +LI = "0000000000H2QYCGPCWQM1JSGS" +WANG = "0000000000RWG40ESZPGJT629J" +ZHAO = "00000000004CQV7WASJA6N6FW4" + + +def upgrade() -> None: + op.execute( + f""" + INSERT INTO roles + (role_id, role_code, role_name, role_scope, is_builtin) + VALUES + ('{ADMIN_ROLE}', 'admin', '管理员', 'workspace', 1), + ('{DEVELOPER_ROLE}', 'developer', '开发人员', 'workspace', 1) + ON DUPLICATE KEY UPDATE + role_name = VALUES(role_name), + role_scope = VALUES(role_scope) + """ + ) + op.execute( + f""" + INSERT INTO users + (user_id, username, display_name, password_hash, status, email) + VALUES + ('{ZHANG}', 'admin-zhang', '张三', 'demo-login-disabled', 'active', + 'zhangsan@example.local'), + ('{LI}', 'admin-li', '李四', 'demo-login-disabled', 'active', + 'lisi@example.local'), + ('{WANG}', 'dev-wang', '王五', 'demo-login-disabled', 'active', + 'wangwu@example.local'), + ('{ZHAO}', 'dev-zhao', '赵六', 'demo-login-disabled', 'active', + 'zhaoliu@example.local') + ON DUPLICATE KEY UPDATE + username = VALUES(username), + display_name = VALUES(display_name), + status = 'active', + email = VALUES(email) + """ + ) + op.execute( + f""" + INSERT INTO workspaces + (workspace_id, workspace_code, workspace_name, active_root_uri, + quota_bytes, used_bytes, status, created_by, description, + artifact_bucket, artifact_prefix) + VALUES + ('{MODEL_WORKSPACE}', 'model-dev', '模型开发 Workspace', + 'file:///workspace/workspaces/model-dev', 0, 0, 'active', + '{ZHANG}', '模型开发与脚本调度', 'model-platform', + 'workspaces/model-dev'), + ('{RISK_WORKSPACE}', 'risk-validation', '风险验证 Workspace', + 'file:///workspace/workspaces/risk-validation', 0, 0, 'active', + '{LI}', '风险模型验证与批处理', 'model-platform', + 'workspaces/risk-validation') + ON DUPLICATE KEY UPDATE + workspace_name = VALUES(workspace_name), + active_root_uri = VALUES(active_root_uri), + status = 'active', + description = VALUES(description) + """ + ) + values = [] + for workspace_id in (MODEL_WORKSPACE, RISK_WORKSPACE): + for user_id, role_id in ( + (ZHANG, ADMIN_ROLE), + (LI, ADMIN_ROLE), + (WANG, DEVELOPER_ROLE), + (ZHAO, DEVELOPER_ROLE), + ): + values.append( + f"('{workspace_id}', '{user_id}', '{role_id}', 'active')" + ) + op.execute( + """ + INSERT INTO workspace_members + (workspace_id, user_id, role_id, member_status) + VALUES + """ + + ",\n".join(values) + + """ + ON DUPLICATE KEY UPDATE + role_id = VALUES(role_id), + member_status = 'active' + """ + ) + + +def downgrade() -> None: + # 演示身份可能已产生业务数据,降级时保留,避免破坏外键引用。 + pass diff --git a/migrations/versions/20260728_0003_schedule_artifact_visibility.py b/migrations/versions/20260728_0003_schedule_artifact_visibility.py new file mode 100644 index 0000000..0cb4f09 --- /dev/null +++ b/migrations/versions/20260728_0003_schedule_artifact_visibility.py @@ -0,0 +1,41 @@ +"""decouple schedule artifact visibility from version history + +Revision ID: 20260728_0003 +Revises: 20260728_0002 +Create Date: 2026-07-28 +""" + +from collections.abc import Sequence + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import mysql + + +revision: str = "20260728_0003" +down_revision: str | Sequence[str] | None = "20260728_0002" +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None + + +def upgrade() -> None: + op.add_column( + "versions", + sa.Column( + "schedule_hidden_at", + mysql.DATETIME(fsp=3), + nullable=True, + comment="从调度稳定版本列表移除的时间;不影响版本和运行历史", + ), + ) + op.create_index( + "idx_versions_schedule_visible", + "versions", + ["workspace_id", "schedule_hidden_at", "created_at"], + unique=False, + ) + + +def downgrade() -> None: + op.drop_index("idx_versions_schedule_visible", table_name="versions") + op.drop_column("versions", "schedule_hidden_at") diff --git a/migrations/versions/README.md b/migrations/versions/README.md new file mode 100644 index 0000000..28f2e12 --- /dev/null +++ b/migrations/versions/README.md @@ -0,0 +1,7 @@ +# Migration Revisions + +该目录只保存经过评审和验证的 Alembic 迁移版本。 + +- 已发布迁移不得直接修改。 +- 新迁移必须同时提供可执行的 `upgrade()` 和 `downgrade()`。 +- 自动生成后必须检查数据类型、约束、索引和执行顺序。 diff --git a/nginx/default.conf.template b/nginx/default.conf.template new file mode 100644 index 0000000..7c05630 --- /dev/null +++ b/nginx/default.conf.template @@ -0,0 +1,76 @@ +map $http_upgrade $connection_upgrade { + default upgrade; + '' close; +} + +upstream backend_upstream { + server backend:8000; +} + +upstream runtime_upstream { + server runtime:8000; +} + +upstream jupyter_upstream { + server jupyter:8888; +} + +server { + listen 80; + server_name _; + client_max_body_size 100m; + + location = /health { + default_type application/json; + return 200 '{"status":"ok","service":"gateway"}'; + } + + location /api/ { + proxy_pass http://backend_upstream; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Request-ID $request_id; + } + + location = /_jupyter_auth { + internal; + proxy_pass http://runtime_upstream/internal/v1/jupyter/authorize; + proxy_pass_request_body off; + proxy_set_header Content-Length ""; + proxy_set_header Cookie $http_cookie; + proxy_set_header X-Original-URI $request_uri; + proxy_set_header X-Request-ID $request_id; + proxy_set_header X-Service-Token "${INTERNAL_SERVICE_TOKEN}"; + } + + location = /jupyter { + return 308 /jupyter/; + } + + location ^~ /jupyter/ { + auth_request /_jupyter_auth; + auth_request_set $jupyter_authorization + $upstream_http_x_jupyter_authorization; + + proxy_pass http://jupyter_upstream; + proxy_http_version 1.1; + proxy_set_header Authorization $jupyter_authorization; + proxy_set_header Host $http_host; + proxy_set_header Origin $http_origin; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Prefix /jupyter; + proxy_set_header X-Request-ID $request_id; + proxy_buffering off; + proxy_request_buffering off; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; + proxy_redirect off; + } +} diff --git a/runtime/Dockerfile b/runtime/Dockerfile index 9b6dcf1..447ec8b 100644 --- a/runtime/Dockerfile +++ b/runtime/Dockerfile @@ -1,31 +1,13 @@ -# runtime/Dockerfile -FROM python:3.12-slim +FROM python:3.12-slim-bookworm +ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 PYTHONPATH=/app WORKDIR /app - -# 安装系统依赖(fuse3 是 rclone mount 的核心底层依赖) -RUN apt-get update && apt-get install -y --no-install-recommends \ - fuse3 \ - ca-certificates \ - curl \ - procps \ - && sed -i 's/#user_allow_other/user_allow_other/g' /etc/fuse.conf \ - && rm -rf /var/lib/apt/lists/* - -# 从官方 Rclone 镜像直接复制 rclone 二进制文件(高效、稳定) -COPY --from=rclone/rclone:latest /usr/local/bin/rclone /usr/local/bin/rclone - -# 安装 uv COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/uv - - COPY pyproject.toml uv.lock ./ - COPY common ./common +COPY contracts ./contracts COPY runtime ./runtime +RUN uv sync --frozen --no-dev --no-editable --package runtime -RUN UV_DEFAULT_INDEX="https://pypi.tuna.tsinghua.edu.cn/simple/" uv sync --frozen --no-dev --no-editable --package runtime - -EXPOSE 8001 - -CMD ["uv", "run", "--package", "runtime", "uvicorn", "runtime.main:app", "--host", "0.0.0.0", "--port", "8001"] \ No newline at end of file +EXPOSE 8000 +CMD ["uv", "run", "--frozen", "--package", "runtime", "uvicorn", "runtime.main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/runtime/README.md b/runtime/README.md index e69de29..5a2c3f5 100644 --- a/runtime/README.md +++ b/runtime/README.md @@ -0,0 +1,4 @@ +# Runtime + +独立 Runtime/Jupyter 管理服务。负责 Workspace Runtime、Notebook Session、 +Jupyter 访问票据及可选编辑锁。Demo 默认关闭互斥锁。 diff --git a/runtime/pyproject.toml b/runtime/pyproject.toml index b806237..aee5397 100644 --- a/runtime/pyproject.toml +++ b/runtime/pyproject.toml @@ -1,31 +1,21 @@ [project] name = "runtime" -version = "0.1.0" -description = "Add your description here" -readme = "README.md" -authors = [ - { name = "tao.chen", email = "93983997+taochen-ct@users.noreply.github.com" } -] +version = "0.2.0" requires-python = ">=3.12" dependencies = [ - "fastapi>=0.140.0", - "loguru>=0.7.3", - "notebook>=7.6.1", - "pydantic>=2.13.4", - "uvicorn>=0.51.0", + "common", + "fastapi==0.116.1", + "uvicorn[standard]==0.35.0", + "httpx==0.28.1", + "redis==5.2.1", ] -[project.scripts] -runtime = "runtime:main" - - -[[tool.uv.index]] -url = "https://pypi.tuna.tsinghua.edu.cn/simple/" -default = true - [tool.uv.sources] common = { workspace = true } [build-system] requires = ["hatchling"] -build-backend = "hatchling.build" \ No newline at end of file +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["src/runtime"] diff --git a/runtime/src/runtime/__init__.py b/runtime/src/runtime/__init__.py index 504d779..da2d535 100644 --- a/runtime/src/runtime/__init__.py +++ b/runtime/src/runtime/__init__.py @@ -1,2 +1 @@ -def main() -> None: - print("Hello from runtime!") +"""Runtime Manager application.""" diff --git a/runtime/src/runtime/legacy_process_runtime.py b/runtime/src/runtime/legacy_process_runtime.py new file mode 100644 index 0000000..2e9089d --- /dev/null +++ b/runtime/src/runtime/legacy_process_runtime.py @@ -0,0 +1,449 @@ +# coding=utf-8 +""" +@Time :2026/7/27 +@Author :tao.chen +""" +import os +import secrets +import socket +import subprocess +import time +import datetime +from contextlib import asynccontextmanager +from pathlib import Path +from typing import Dict, Optional +from fastapi import FastAPI, HTTPException +from pydantic import BaseModel, Field +from loguru import logger + +# 全局内存字典:记录运行中的 Jupyter 进程信息 +JUPYTER_PROCESSES: Dict[str, dict] = {} + +WORKSPACES_ROOT = Path(os.getenv("WORKSPACES_ROOT", "/app/workspaces")) +PUBLIC_BASE_URL = os.getenv("PUBLIC_BASE_URL", "http://localhost") +REMOTE_BUCKET = os.getenv("REMOTE_BUCKET", "rustfs:workspaces") +RCLONE_PROCESS = None + + +def is_mountpoint(path: Path) -> bool: + """ + 判断目录是否已经挂载 + """ + result = subprocess.run( + ["mountpoint", "-q", str(path)], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + return result.returncode == 0 + + +def start_rclone_mount(): + """ + 启动 rclone mount + """ + global RCLONE_PROCESS + if is_mountpoint(WORKSPACES_ROOT): + logger.info( + f"Mountpoint already exists: {WORKSPACES_ROOT}" + ) + return + + WORKSPACES_ROOT.mkdir(parents=True, exist_ok=True) + logger.info( + f"Starting rclone mount " + f"{REMOTE_BUCKET} -> {WORKSPACES_ROOT}" + ) + + log_file = open( + "/tmp/rclone-mount.log", + "a", + buffering=1, + ) + + cmd = [ + "rclone", + "mount", + REMOTE_BUCKET, + WORKSPACES_ROOT.as_posix(), + "--allow-other", + "--vfs-cache-mode","full", + "--vfs-cache-max-size","20G", + "--vfs-write-back","5s", + "--dir-cache-time","30s", + "--poll-interval","30s", + "--log-level","INFO", + ] + + RCLONE_PROCESS = subprocess.Popen( + cmd, + stdout=log_file, + stderr=subprocess.STDOUT, + start_new_session=True, + ) + + # 等待 mount ready + timeout = 20 + while timeout > 0: + if is_mountpoint(WORKSPACES_ROOT): + logger.info(f"rclone mount ready: {WORKSPACES_ROOT}" ) + return + + # rclone异常退出 + if RCLONE_PROCESS.poll() is not None: + raise RuntimeError( "rclone mount process exited") + time.sleep(1) + timeout -= 1 + + raise RuntimeError( f"Timeout waiting mount: {WORKSPACES_ROOT}") + + +def stop_rclone_mount(): + global RCLONE_PROCESS + logger.info( + "Stopping rclone mount..." + ) + if RCLONE_PROCESS: + if RCLONE_PROCESS.poll() is None: + RCLONE_PROCESS.terminate() + try: + RCLONE_PROCESS.wait(timeout=10) + except subprocess.TimeoutExpired: + logger.warning("Force killing rclone") + RCLONE_PROCESS.kill() + + if is_mountpoint(WORKSPACES_ROOT): + logger.info(f"Unmount {WORKSPACES_ROOT}") + result = subprocess.run( + [ + "fusermount3", + "-u", + WORKSPACES_ROOT.as_posix(), + ] + ) + if result.returncode != 0: + subprocess.run( + [ + "umount", + "-l", + WORKSPACES_ROOT.as_posix(), + ] + ) + + logger.info("rclone stopped") + + +def scan_workspaces(): + """ + 扫描已有 workspace + """ + if not WORKSPACES_ROOT.exists(): + return + try: + entries = os.listdir(WORKSPACES_ROOT) + except Exception as e: + logger.error(f"scan workspace failed: {e}") + return + + for entry in entries: + path = WORKSPACES_ROOT / entry + if not path.is_dir(): + continue + logger.info(f"Found workspace: {entry}" ) + + try: + full_path = os.path.join(WORKSPACES_ROOT, entry) + if os.path.isdir(full_path): + logger.info(f"Auto-starting Jupyter for workspace ID: '{entry}'") + try: + _handle_start(entry) + except Exception as err: + logger.error(f"Startup failed for workspace '{entry}': {err}") + except Exception as e: + logger.error(f"Start workspace {entry} failed: {e}" ) + + +def get_free_port() -> int: + """利用操作系统 socket 特性,动态获取当前闲置的可用端口""" + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + s.bind(("", 0)) + s.listen(1) + port = s.getsockname()[1] + return port + + +# 统一请求 Model +class JupyterActionRequest(BaseModel): + action: str = Field( + ..., description="操作类型: 'start' | 'stop' | 'list'" + ) + workspace_id: Optional[str] = Field( + None, description="Workspace ID (start 和 stop 操作时必填)" + ) + + +# 辅助处理函数:启动逻辑 +def start_process(cmd, workspace_path, log_dir="/tmp/process_logs"): + log_dir = Path(log_dir) + log_dir.mkdir(parents=True, exist_ok=True) + + start_time = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") + + # 临时日志文件 + temp_log = log_dir / f"process_start_{start_time}.log" + + log_file = open(temp_log, "a", buffering=1) + + process = subprocess.Popen( + cmd, + cwd=workspace_path, + stdout=log_file, + stderr=subprocess.STDOUT, # stderr 合并到 stdout + start_new_session=True, + ) + + # 根据真实 pid 重命名 + final_log = log_dir / f"process_{process.pid}_{start_time}.log" + log_file.close() + + temp_log.rename(final_log) + + logger.info( + f"process started pid={process.pid}, log={final_log}" + ) + + return process + + +def _handle_start(ws_id: str): + workspace_path = WORKSPACES_ROOT / ws_id + + # 如果已存在,校验进程状态并复用 + if ws_id in JUPYTER_PROCESSES: + p_info = JUPYTER_PROCESSES[ws_id] + if p_info["process"].poll() is None: + logger.info(f"Workspace {ws_id} already running.") + return { + "status": "running", + "workspace_id": ws_id, + "port": p_info["port"], + "full_url": p_info["full_url"], + } + else: + del JUPYTER_PROCESSES[ws_id] + + # 2. 动态申请端口与 Token + port = get_free_port() + token = secrets.token_hex(16) + base_path = f"/jupyter/{ws_id}/" + + cmd = [ + "jupyter", + "notebook", + f"--port={port}", + "--ip=0.0.0.0", + "--no-browser", + "--allow-root", + f"--ServerApp.token={token}", + f"--ServerApp.base_url={base_path}", + "--notebook-dir=.", + # 适用于现代 Jupyter Server / JupyterLab + "--ServerApp.terminals_enabled=False", + # 兼容经典 Notebook / 旧版配置项 + "--NotebookApp.terminals_enabled=False", + # 允许 Nginx 跨域代理与 WebSocket 通信(关键) + "--ServerApp.allow_origin=*", + "--NotebookApp.allow_origin=*", + "--ServerApp.disable_check_xsrf=True", + "--NotebookApp.disable_check_xsrf=True" + ] + + try: + process = start_process(cmd, workspace_path.as_posix()) + + full_url = f"{PUBLIC_BASE_URL}:{port}{base_path}?token={token}" + + JUPYTER_PROCESSES[ws_id] = { + "process": process, + "base_url": PUBLIC_BASE_URL, + "port": port, + "token": token, + "full_url": full_url, + "started_at": time.time(), + } + + logger.info( + f"Started Jupyter for workspace {ws_id} on port {port}" + ) + return { + "pid": process.pid, + "base_url": PUBLIC_BASE_URL, + "status": "success", + "workspace_id": ws_id, + "port": port, + "token": token, + } + except Exception as e: + logger.error(f"Failed to start Jupyter for {ws_id}: {str(e)}") + raise HTTPException( + status_code=500, detail=f"Failed to start Jupyter: {str(e)}" + ) + + +# 辅助处理函数:停止逻辑 +def _handle_stop(ws_id: str): + if ws_id not in JUPYTER_PROCESSES: + raise HTTPException( + status_code=404, + detail=f"No active Jupyter process found for workspace '{ws_id}'", + ) + + p_info = JUPYTER_PROCESSES[ws_id] + process: subprocess.Popen = p_info["process"] + + if process.poll() is None: + try: + process.terminate() + process.wait(timeout=3) + logger.info( + f"Gracefully stopped Jupyter for workspace {ws_id}" + ) + except subprocess.TimeoutExpired: + logger.warning( + f"Jupyter for {ws_id} did not stop gracefully. Force killing..." + ) + process.kill() + process.wait() + + del JUPYTER_PROCESSES[ws_id] + return { + "status": "stopped", + "workspace_id": ws_id, + "message": "Jupyter process terminated and port released.", + } + + +# 辅助处理函数:列表查询逻辑 +def _handle_list(): + active_instances = {} + for ws_id, info in list(JUPYTER_PROCESSES.items()): + is_alive = info["process"].poll() is None + active_instances[ws_id] = { + "port": info["port"], + "full_url": info["full_url"], + "is_alive": is_alive, + } + return {"status": "success", "instances": active_instances} + + +def _handle_get(ws_id: str): + """【新增】获取指定 Workspace 的 Jupyter 运行状态与完整 URL""" + if ws_id not in JUPYTER_PROCESSES: + raise HTTPException( + status_code=404, + detail=f"No active Jupyter process found for workspace '{ws_id}'", + ) + + p_info = JUPYTER_PROCESSES[ws_id] + is_alive = p_info["process"].poll() is None + + if not is_alive: + # 进程如果挂了,清理内存字典并报 404 + del JUPYTER_PROCESSES[ws_id] + raise HTTPException( + status_code=404, + detail=f"Jupyter process for workspace '{ws_id}' has terminated unexpectedly.", + ) + + return { + "status": "running", + "pid": p_info["process"].pid, + "base_url": PUBLIC_BASE_URL, + "workspace_id": ws_id, + "port": p_info["port"], + "token": p_info["token"], + "started_at": p_info["started_at"], + } + + +# ==================== FastAPI Lifespan 定义 ==================== +@asynccontextmanager +async def lifespan(app: FastAPI): + global RCLONE_PROCESS + logger.info("Starting up Runtime Service...") + start_rclone_mount() + logger.info(f"Scanning workspaces: {WORKSPACES_ROOT}") + scan_workspaces() + logger.info("Runtime Service started") + + # ==================== 2. 服务运行阶段 (Serving) ==================== + try: + yield # 服务保持运行,等待并处理 API 请求 + finally: + logger.info("Service is shutting down. Terminating all active Jupyter sub-processes...") + + # 优先杀死所有 Jupyter 子进程(确保文件句柄被释放) + active_workspaces = list(JUPYTER_PROCESSES.keys()) + for ws_id in active_workspaces: + try: + _handle_stop(ws_id) + except Exception as err: + logger.error(f"Error terminating Jupyter process for '{ws_id}': {err}") + logger.info("All Jupyter sub-processes have been terminated.") + JUPYTER_PROCESSES.clear() + + # 卸载 Rclone 挂载点(强制将 VFS 缓存刷新同步至对象存储) + try: + stop_rclone_mount() + except Exception as e: + logger.error(f"Stop rclone failed: {e}" ) + logger.info("Runtime Service stopped") + +app = FastAPI(lifespan=lifespan) + + +# ---------------- 统一入口 POST 接口 ---------------- +@app.get("/api/v1/health") +def healthz(): + return {"status": "ok"} + + +@app.post("/api/v1/jupyter") +def handle_jupyter_action(req: JupyterActionRequest): + action = req.action.lower() + + # 1. 启动操作 + if action == "start": + if not req.workspace_id: + raise HTTPException( + status_code=400, + detail="'workspace_id' is required when action='start'", + ) + return _handle_start(req.workspace_id) + + # 2. 停止操作 + elif action == "stop": + if not req.workspace_id: + raise HTTPException( + status_code=400, + detail="'workspace_id' is required when action='stop'", + ) + return _handle_stop(req.workspace_id) + + # 3. 列表操作 + elif action == "list": + return _handle_list() + + elif action == "get": + if not req.workspace_id: + raise HTTPException( + status_code=400, + detail="'workspace_id' is required for action='get'", + ) + return _handle_get(req.workspace_id) + + # 未知操作 + else: + raise HTTPException( + status_code=400, + detail=f"Invalid action '{req.action}'. Supported actions: 'start', 'stop', 'list'", + ) diff --git a/runtime/src/runtime/main.py b/runtime/src/runtime/main.py index 2e9089d..7e272cc 100644 --- a/runtime/src/runtime/main.py +++ b/runtime/src/runtime/main.py @@ -1,449 +1,1146 @@ -# coding=utf-8 -""" -@Time :2026/7/27 -@Author :tao.chen -""" +from __future__ import annotations + +import asyncio +import hashlib +import json +import logging import os import secrets -import socket -import subprocess -import time -import datetime from contextlib import asynccontextmanager -from pathlib import Path -from typing import Dict, Optional -from fastapi import FastAPI, HTTPException -from pydantic import BaseModel, Field -from loguru import logger +from datetime import UTC, datetime, timedelta +from pathlib import PurePosixPath +from typing import Any, AsyncIterator +from urllib.parse import quote, unquote, urlsplit -# 全局内存字典:记录运行中的 Jupyter 进程信息 -JUPYTER_PROCESSES: Dict[str, dict] = {} +import httpx +from fastapi import ( + Cookie, + Depends, + Header, + HTTPException, + Query, + Request, + Response, + status, +) +from redis.asyncio import Redis +from sqlalchemy import select, update +from sqlalchemy.ext.asyncio import AsyncSession -WORKSPACES_ROOT = Path(os.getenv("WORKSPACES_ROOT", "/app/workspaces")) -PUBLIC_BASE_URL = os.getenv("PUBLIC_BASE_URL", "http://localhost") -REMOTE_BUCKET = os.getenv("REMOTE_BUCKET", "rustfs:workspaces") -RCLONE_PROCESS = None +from common.db import create_database_engine, create_session_factory +from common.db.models import ( + EditSessions, + Roles, + RuntimeInstances, + StorageObjects, + Users, + WorkspaceMembers, + Workspaces, +) +from common.ids import new_ulid +from common.service_app import create_service_app +from runtime.redis_lock import ( + acquire as redis_acquire, + current as redis_current, + heartbeat as redis_heartbeat, + lock_key, + release as redis_release, +) +from runtime.providers.shared_jupyter import ( + RuntimeProviderError, + SharedJupyterAdapter, +) +from runtime.runtime_lifecycle import ( + RuntimeLifecycle, + runtime_payload, +) +from runtime.schemas import ( + AcquireFileLockRequest, + CreateRuntimeSessionApiRequest, + EnsureRuntimeApiRequest, + FileLockTokenRequest, + RuntimeIdentityRequest, + StopRuntimeApiRequest, +) + +logger = logging.getLogger(__name__) -def is_mountpoint(path: Path) -> bool: - """ - 判断目录是否已经挂载 - """ - result = subprocess.run( - ["mountpoint", "-q", str(path)], - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, - ) - return result.returncode == 0 +def utcnow() -> datetime: + return datetime.now(UTC).replace(tzinfo=None) -def start_rclone_mount(): - """ - 启动 rclone mount - """ - global RCLONE_PROCESS - if is_mountpoint(WORKSPACES_ROOT): - logger.info( - f"Mountpoint already exists: {WORKSPACES_ROOT}" +def utc_iso(value: datetime) -> str: + return value.replace(tzinfo=UTC).isoformat().replace("+00:00", "Z") + + +def token_digest(value: str) -> bytes: + return hashlib.sha256(value.encode("utf-8")).digest() + + +def jupyter_ticket_key(raw_ticket: str) -> str: + digest = hashlib.sha256(raw_ticket.encode("utf-8")).hexdigest() + return f"jupyter:access-ticket:{digest}" + + +def jupyter_url(workspace_code: str, relative_path: str) -> str: + relative = PurePosixPath(relative_path.replace("\\", "/")) + if ( + relative.is_absolute() + or not relative.parts + or any(part in {"", ".", ".."} for part in relative.parts) + ): + raise lock_error( + status.HTTP_409_CONFLICT, + "JUPYTER_PATH_INVALID", + "Notebook 路径无效", ) - return - - WORKSPACES_ROOT.mkdir(parents=True, exist_ok=True) - logger.info( - f"Starting rclone mount " - f"{REMOTE_BUCKET} -> {WORKSPACES_ROOT}" - ) - - log_file = open( - "/tmp/rclone-mount.log", - "a", - buffering=1, - ) - - cmd = [ - "rclone", - "mount", - REMOTE_BUCKET, - WORKSPACES_ROOT.as_posix(), - "--allow-other", - "--vfs-cache-mode","full", - "--vfs-cache-max-size","20G", - "--vfs-write-back","5s", - "--dir-cache-time","30s", - "--poll-interval","30s", - "--log-level","INFO", - ] - - RCLONE_PROCESS = subprocess.Popen( - cmd, - stdout=log_file, - stderr=subprocess.STDOUT, - start_new_session=True, - ) - - # 等待 mount ready - timeout = 20 - while timeout > 0: - if is_mountpoint(WORKSPACES_ROOT): - logger.info(f"rclone mount ready: {WORKSPACES_ROOT}" ) - return - - # rclone异常退出 - if RCLONE_PROCESS.poll() is not None: - raise RuntimeError( "rclone mount process exited") - time.sleep(1) - timeout -= 1 - - raise RuntimeError( f"Timeout waiting mount: {WORKSPACES_ROOT}") + path = PurePosixPath(workspace_code, *relative.parts).as_posix() + encoded_path = quote(path, safe="/") + if relative.suffix.lower() == ".ipynb": + return f"/jupyter/notebooks/{encoded_path}" + return f"/jupyter/lab/tree/{encoded_path}" -def stop_rclone_mount(): - global RCLONE_PROCESS - logger.info( - "Stopping rclone mount..." - ) - if RCLONE_PROCESS: - if RCLONE_PROCESS.poll() is None: - RCLONE_PROCESS.terminate() - try: - RCLONE_PROCESS.wait(timeout=10) - except subprocess.TimeoutExpired: - logger.warning("Force killing rclone") - RCLONE_PROCESS.kill() - - if is_mountpoint(WORKSPACES_ROOT): - logger.info(f"Unmount {WORKSPACES_ROOT}") - result = subprocess.run( - [ - "fusermount3", - "-u", - WORKSPACES_ROOT.as_posix(), - ] - ) - if result.returncode != 0: - subprocess.run( - [ - "umount", - "-l", - WORKSPACES_ROOT.as_posix(), - ] - ) - - logger.info("rclone stopped") - - -def scan_workspaces(): - """ - 扫描已有 workspace - """ - if not WORKSPACES_ROOT.exists(): - return - try: - entries = os.listdir(WORKSPACES_ROOT) - except Exception as e: - logger.error(f"scan workspace failed: {e}") - return - - for entry in entries: - path = WORKSPACES_ROOT / entry - if not path.is_dir(): - continue - logger.info(f"Found workspace: {entry}" ) - - try: - full_path = os.path.join(WORKSPACES_ROOT, entry) - if os.path.isdir(full_path): - logger.info(f"Auto-starting Jupyter for workspace ID: '{entry}'") - try: - _handle_start(entry) - except Exception as err: - logger.error(f"Startup failed for workspace '{entry}': {err}") - except Exception as e: - logger.error(f"Start workspace {entry} failed: {e}" ) - - -def get_free_port() -> int: - """利用操作系统 socket 特性,动态获取当前闲置的可用端口""" - with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: - s.bind(("", 0)) - s.listen(1) - port = s.getsockname()[1] - return port - - -# 统一请求 Model -class JupyterActionRequest(BaseModel): - action: str = Field( - ..., description="操作类型: 'start' | 'stop' | 'list'" - ) - workspace_id: Optional[str] = Field( - None, description="Workspace ID (start 和 stop 操作时必填)" +def lock_error( + status_code: int, + code: str, + message: str, + *, + retryable: bool = False, + details: dict[str, Any] | None = None, +) -> HTTPException: + return HTTPException( + status_code, + { + "code": code, + "message": message, + "retryable": retryable, + "details": details or {}, + }, ) -# 辅助处理函数:启动逻辑 -def start_process(cmd, workspace_path, log_dir="/tmp/process_logs"): - log_dir = Path(log_dir) - log_dir.mkdir(parents=True, exist_ok=True) - - start_time = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") - - # 临时日志文件 - temp_log = log_dir / f"process_start_{start_time}.log" - - log_file = open(temp_log, "a", buffering=1) - - process = subprocess.Popen( - cmd, - cwd=workspace_path, - stdout=log_file, - stderr=subprocess.STDOUT, # stderr 合并到 stdout - start_new_session=True, - ) - - # 根据真实 pid 重命名 - final_log = log_dir / f"process_{process.pid}_{start_time}.log" - log_file.close() - - temp_log.rename(final_log) - - logger.info( - f"process started pid={process.pid}, log={final_log}" - ) - - return process - - -def _handle_start(ws_id: str): - workspace_path = WORKSPACES_ROOT / ws_id - - # 如果已存在,校验进程状态并复用 - if ws_id in JUPYTER_PROCESSES: - p_info = JUPYTER_PROCESSES[ws_id] - if p_info["process"].poll() is None: - logger.info(f"Workspace {ws_id} already running.") - return { - "status": "running", - "workspace_id": ws_id, - "port": p_info["port"], - "full_url": p_info["full_url"], - } - else: - del JUPYTER_PROCESSES[ws_id] - - # 2. 动态申请端口与 Token - port = get_free_port() - token = secrets.token_hex(16) - base_path = f"/jupyter/{ws_id}/" - - cmd = [ - "jupyter", - "notebook", - f"--port={port}", - "--ip=0.0.0.0", - "--no-browser", - "--allow-root", - f"--ServerApp.token={token}", - f"--ServerApp.base_url={base_path}", - "--notebook-dir=.", - # 适用于现代 Jupyter Server / JupyterLab - "--ServerApp.terminals_enabled=False", - # 兼容经典 Notebook / 旧版配置项 - "--NotebookApp.terminals_enabled=False", - # 允许 Nginx 跨域代理与 WebSocket 通信(关键) - "--ServerApp.allow_origin=*", - "--NotebookApp.allow_origin=*", - "--ServerApp.disable_check_xsrf=True", - "--NotebookApp.disable_check_xsrf=True" - ] - - try: - process = start_process(cmd, workspace_path.as_posix()) - - full_url = f"{PUBLIC_BASE_URL}:{port}{base_path}?token={token}" - - JUPYTER_PROCESSES[ws_id] = { - "process": process, - "base_url": PUBLIC_BASE_URL, - "port": port, - "token": token, - "full_url": full_url, - "started_at": time.time(), - } - - logger.info( - f"Started Jupyter for workspace {ws_id} on port {port}" - ) - return { - "pid": process.pid, - "base_url": PUBLIC_BASE_URL, - "status": "success", - "workspace_id": ws_id, - "port": port, - "token": token, - } - except Exception as e: - logger.error(f"Failed to start Jupyter for {ws_id}: {str(e)}") +def verify_internal_service( + x_service_token: str = Header(alias="X-Service-Token"), +) -> None: + expected = os.environ.get("INTERNAL_SERVICE_TOKEN", "") + if not expected or not secrets.compare_digest(expected, x_service_token): raise HTTPException( - status_code=500, detail=f"Failed to start Jupyter: {str(e)}" + status.HTTP_401_UNAUTHORIZED, + "invalid internal service identity", ) -# 辅助处理函数:停止逻辑 -def _handle_stop(ws_id: str): - if ws_id not in JUPYTER_PROCESSES: - raise HTTPException( - status_code=404, - detail=f"No active Jupyter process found for workspace '{ws_id}'", - ) - - p_info = JUPYTER_PROCESSES[ws_id] - process: subprocess.Popen = p_info["process"] - - if process.poll() is None: - try: - process.terminate() - process.wait(timeout=3) - logger.info( - f"Gracefully stopped Jupyter for workspace {ws_id}" - ) - except subprocess.TimeoutExpired: - logger.warning( - f"Jupyter for {ws_id} did not stop gracefully. Force killing..." - ) - process.kill() - process.wait() - - del JUPYTER_PROCESSES[ws_id] - return { - "status": "stopped", - "workspace_id": ws_id, - "message": "Jupyter process terminated and port released.", - } - - -# 辅助处理函数:列表查询逻辑 -def _handle_list(): - active_instances = {} - for ws_id, info in list(JUPYTER_PROCESSES.items()): - is_alive = info["process"].poll() is None - active_instances[ws_id] = { - "port": info["port"], - "full_url": info["full_url"], - "is_alive": is_alive, - } - return {"status": "success", "instances": active_instances} - - -def _handle_get(ws_id: str): - """【新增】获取指定 Workspace 的 Jupyter 运行状态与完整 URL""" - if ws_id not in JUPYTER_PROCESSES: - raise HTTPException( - status_code=404, - detail=f"No active Jupyter process found for workspace '{ws_id}'", - ) - - p_info = JUPYTER_PROCESSES[ws_id] - is_alive = p_info["process"].poll() is None - - if not is_alive: - # 进程如果挂了,清理内存字典并报 404 - del JUPYTER_PROCESSES[ws_id] - raise HTTPException( - status_code=404, - detail=f"Jupyter process for workspace '{ws_id}' has terminated unexpectedly.", - ) - - return { - "status": "running", - "pid": p_info["process"].pid, - "base_url": PUBLIC_BASE_URL, - "workspace_id": ws_id, - "port": p_info["port"], - "token": p_info["token"], - "started_at": p_info["started_at"], - } - - -# ==================== FastAPI Lifespan 定义 ==================== @asynccontextmanager -async def lifespan(app: FastAPI): - global RCLONE_PROCESS - logger.info("Starting up Runtime Service...") - start_rclone_mount() - logger.info(f"Scanning workspaces: {WORKSPACES_ROOT}") - scan_workspaces() - logger.info("Runtime Service started") - - # ==================== 2. 服务运行阶段 (Serving) ==================== - try: - yield # 服务保持运行,等待并处理 API 请求 - finally: - logger.info("Service is shutting down. Terminating all active Jupyter sub-processes...") - - # 优先杀死所有 Jupyter 子进程(确保文件句柄被释放) - active_workspaces = list(JUPYTER_PROCESSES.keys()) - for ws_id in active_workspaces: - try: - _handle_stop(ws_id) - except Exception as err: - logger.error(f"Error terminating Jupyter process for '{ws_id}': {err}") - logger.info("All Jupyter sub-processes have been terminated.") - JUPYTER_PROCESSES.clear() - - # 卸载 Rclone 挂载点(强制将 VFS 缓存刷新同步至对象存储) - try: - stop_rclone_mount() - except Exception as e: - logger.error(f"Stop rclone failed: {e}" ) - logger.info("Runtime Service stopped") - -app = FastAPI(lifespan=lifespan) - - -# ---------------- 统一入口 POST 接口 ---------------- -@app.get("/api/v1/health") -def healthz(): - return {"status": "ok"} - - -@app.post("/api/v1/jupyter") -def handle_jupyter_action(req: JupyterActionRequest): - action = req.action.lower() - - # 1. 启动操作 - if action == "start": - if not req.workspace_id: - raise HTTPException( - status_code=400, - detail="'workspace_id' is required when action='start'", - ) - return _handle_start(req.workspace_id) - - # 2. 停止操作 - elif action == "stop": - if not req.workspace_id: - raise HTTPException( - status_code=400, - detail="'workspace_id' is required when action='stop'", - ) - return _handle_stop(req.workspace_id) - - # 3. 列表操作 - elif action == "list": - return _handle_list() - - elif action == "get": - if not req.workspace_id: - raise HTTPException( - status_code=400, - detail="'workspace_id' is required for action='get'", - ) - return _handle_get(req.workspace_id) - - # 未知操作 - else: - raise HTTPException( - status_code=400, - detail=f"Invalid action '{req.action}'. Supported actions: 'start', 'stop', 'list'", +async def lifespan(app: Any) -> AsyncIterator[None]: + engine = create_database_engine(os.environ["DATABASE_URL"]) + app.state.session_factory = create_session_factory(engine) + app.state.file_lock_ttl_ms = int( + os.getenv("FILE_LOCK_TTL_MS", "45000") + ) + app.state.file_lock_enabled = ( + os.getenv("FILE_LOCK_ENABLED", "true").strip().lower() + not in {"0", "false", "no", "off"} + ) + if not 5_000 <= app.state.file_lock_ttl_ms <= 300_000: + raise RuntimeError("FILE_LOCK_TTL_MS must be between 5000 and 300000") + app.state.jupyter_ticket_ttl_seconds = int( + os.getenv("JUPYTER_TICKET_TTL_SECONDS", "60") + ) + if not 30 <= app.state.jupyter_ticket_ttl_seconds <= 300: + raise RuntimeError( + "JUPYTER_TICKET_TTL_SECONDS must be between 30 and 300" ) + app.state.redis_client = Redis( + host=os.getenv("REDIS_HOST", "redis"), + port=int(os.getenv("REDIS_PORT", "6379")), + password=os.getenv("REDIS_PASSWORD"), + decode_responses=True, + ) + await app.state.redis_client.ping() + jupyter_internal_url = os.getenv( + "JUPYTER_INTERNAL_URL", + "http://jupyter:8888/jupyter/", + ).rstrip("/") + "/" + app.state.jupyter_http_client = httpx.AsyncClient( + base_url=jupyter_internal_url, + timeout=httpx.Timeout(15.0), + headers={ + "Authorization": f"Bearer {os.environ['JUPYTER_TOKEN']}", + }, + ) + app.state.jupyter_token = os.environ["JUPYTER_TOKEN"] + adapter = SharedJupyterAdapter( + app.state.jupyter_http_client, + internal_url=jupyter_internal_url, + proxy_base_path=os.getenv( + "JUPYTER_PROXY_BASE_PATH", + "/jupyter/", + ), + ) + app.state.runtime_lifecycle = RuntimeLifecycle( + adapter, + lease_seconds=int(os.getenv("RUNTIME_LEASE_SECONDS", "1800")), + ) + cleanup_task = asyncio.create_task( + reconcile_expired_edit_sessions(app) + ) + try: + yield + finally: + cleanup_task.cancel() + try: + await cleanup_task + except asyncio.CancelledError: + pass + await app.state.jupyter_http_client.aclose() + await app.state.redis_client.aclose() + await engine.dispose() + + +app = create_service_app( + os.getenv("SERVICE_NAME", "runtime-manager"), + lifespan=lifespan, +) + + +async def active_member( + session: AsyncSession, + *, + workspace_id: str, + user_id: str, +) -> tuple[Users, Roles, Workspaces]: + statement = ( + select(Users, Roles, Workspaces) + .join( + WorkspaceMembers, + WorkspaceMembers.user_id == Users.user_id, + ) + .join(Roles, Roles.role_id == WorkspaceMembers.role_id) + .join( + Workspaces, + Workspaces.workspace_id == WorkspaceMembers.workspace_id, + ) + .where( + Users.user_id == user_id, + Users.status == "active", + WorkspaceMembers.workspace_id == workspace_id, + WorkspaceMembers.member_status == "active", + Workspaces.status == "active", + ) + ) + row = (await session.execute(statement)).one_or_none() + if row is None: + raise lock_error( + status.HTTP_403_FORBIDDEN, + "WORKSPACE_ACCESS_DENIED", + "用户不是有效的 Workspace 成员", + ) + return row + + +async def editable_object( + session: AsyncSession, + payload: AcquireFileLockRequest, + role: Roles, +) -> StorageObjects: + item = await session.get(StorageObjects, payload.storage_object_id) + if ( + item is None + or item.workspace_id != payload.workspace_id + or item.object_status != "available" + ): + raise lock_error( + status.HTTP_404_NOT_FOUND, + "FILE_NOT_FOUND", + "文件不存在", + ) + if ( + item.object_type != "file" + or item.storage_backend != "workspace_fs" + or bool(item.is_immutable) + ): + raise lock_error( + status.HTTP_409_CONFLICT, + "FILE_NOT_EDITABLE", + "仅 Workspace 中的可变文件可以申请编辑锁", + ) + is_owner = item.owner_user_id == payload.user_id + is_shared_editor = role.role_code != "viewer" + if not (is_owner or is_shared_editor or role.role_code == "admin"): + raise lock_error( + status.HTTP_403_FORBIDDEN, + "FILE_EDIT_DENIED", + "当前用户没有该文件的编辑权限", + ) + return item + + +def session_payload( + item: EditSessions, + *, + lock_token: str | None = None, + relative_path: str | None = None, + heartbeat_interval_seconds: int = 15, + jupyter_url: str | None = None, +) -> dict[str, Any]: + data: dict[str, Any] = { + "edit_session_id": item.edit_session_id, + "workspace_id": item.workspace_id, + "storage_object_id": item.storage_object_id, + "user_id": item.user_id, + "session_status": item.session_status, + "lease_seconds": 45, + "heartbeat_interval_seconds": heartbeat_interval_seconds, + "expires_at": utc_iso(item.expires_at), + "runtime_id": item.runtime_id, + "jupyter_session_id": item.jupyter_session_id, + "jupyter_url": jupyter_url, + } + if lock_token is not None: + data["lock_token"] = lock_token + if relative_path is not None: + data["relative_path"] = relative_path + return data + + +async def owned_session( + session: AsyncSession, + *, + edit_session_id: str, + workspace_id: str, + user_id: str, +) -> EditSessions: + item = await session.get(EditSessions, edit_session_id) + if ( + item is None + or item.workspace_id != workspace_id + or item.user_id != user_id + ): + raise lock_error( + status.HTTP_404_NOT_FOUND, + "FILE_LOCK_NOT_FOUND", + "编辑会话不存在", + ) + return item + + +def verify_token(item: EditSessions, raw_token: str) -> str: + digest = token_digest(raw_token) + if not secrets.compare_digest(item.lock_token_hash, digest): + raise lock_error( + status.HTTP_403_FORBIDDEN, + "FILE_LOCK_TOKEN_INVALID", + "编辑锁 token 无效", + ) + return digest.hex() + + +@app.post( + "/internal/v1/file-locks/acquire", + status_code=status.HTTP_201_CREATED, + dependencies=[Depends(verify_internal_service)], +) +async def acquire_file_lock( + payload: AcquireFileLockRequest, + request: Request, +) -> dict[str, Any]: + ttl_ms: int = request.app.state.file_lock_ttl_ms + now = utcnow() + edit_session_id = new_ulid() + raw_token = secrets.token_urlsafe(32) + digest = token_digest(raw_token) + key = lock_key(payload.workspace_id, payload.storage_object_id) + if not request.app.state.file_lock_enabled: + key = f"{key}:session:{edit_session_id}" + + async with request.app.state.session_factory() as session: + user, role, workspace = await active_member( + session, + workspace_id=payload.workspace_id, + user_id=payload.user_id, + ) + storage_object = await editable_object(session, payload, role) + acquired = await redis_acquire( + request.app.state.redis_client, + key=key, + value={ + "edit_session_id": edit_session_id, + "user_id": payload.user_id, + "display_name": user.display_name, + "token_hash": digest.hex(), + "acquired_at": utc_iso(now), + }, + ttl_ms=ttl_ms, + ) + if not acquired: + current, remaining_ms = await redis_current( + request.app.state.redis_client, + key, + ) + lease_expires_at = utcnow() + timedelta( + milliseconds=max(remaining_ms, 0) + ) + raise lock_error( + status.HTTP_409_CONFLICT, + "FILE_LOCK_CONFLICT", + "文件正在被其他用户编辑", + retryable=True, + details={ + "edit_session_id": ( + current or {} + ).get("edit_session_id"), + "editor_user_id": (current or {}).get("user_id"), + "editor_name": (current or {}).get("display_name"), + "lease_expires_at": utc_iso(lease_expires_at), + }, + ) + + runtime_session = None + try: + runtime_item, _ = ( + await request.app.state.runtime_lifecycle.ensure_running( + session, + workspace=workspace, + user_id=payload.user_id, + request_id=payload.request_id, + ) + ) + if not storage_object.relative_path: + raise RuntimeProviderError( + "Workspace file has no relative path" + ) + runtime_session = ( + await request.app.state.runtime_lifecycle.create_session( + runtime_item, + workspace_code=workspace.workspace_code, + relative_path=storage_object.relative_path, + ) + ) + expires_at = now + timedelta(milliseconds=ttl_ms) + item = EditSessions( + edit_session_id=edit_session_id, + workspace_id=payload.workspace_id, + storage_object_id=payload.storage_object_id, + user_id=payload.user_id, + runtime_id=runtime_item.runtime_id, + jupyter_session_id=runtime_session.session_id, + redis_lock_key=key, + lock_token_hash=digest, + session_status="active", + started_at=now, + last_heartbeat_at=now, + expires_at=expires_at, + ) + if request.app.state.file_lock_enabled: + await session.execute( + update(EditSessions) + .where( + EditSessions.workspace_id == payload.workspace_id, + EditSessions.storage_object_id + == payload.storage_object_id, + EditSessions.session_status == "active", + ) + .values( + session_status="expired", + ended_at=now, + end_reason="redis_lease_expired", + ) + ) + session.add(item) + await session.commit() + except Exception as exc: + await session.rollback() + if runtime_session is not None and not runtime_session.reused: + try: + await request.app.state.runtime_lifecycle.terminate_session( + runtime_session.runtime_id, + runtime_session.session_id, + ) + except RuntimeProviderError: + logger.warning( + "failed to compensate Jupyter session creation", + exc_info=True, + ) + await redis_release( + request.app.state.redis_client, + key=key, + edit_session_id=edit_session_id, + token_hash=digest.hex(), + ) + if isinstance(exc, RuntimeProviderError): + raise lock_error( + status.HTTP_503_SERVICE_UNAVAILABLE, + "JUPYTER_UNAVAILABLE", + "Jupyter Runtime 暂时不可用", + retryable=True, + details={"provider_error": str(exc)}, + ) from exc + raise + + return { + "data": session_payload( + item, + lock_token=raw_token, + relative_path=storage_object.relative_path, + heartbeat_interval_seconds=max(1, ttl_ms // 3000), + jupyter_url=runtime_session.jupyter_url, + ) + } + + +@app.post( + "/internal/v1/file-locks/{edit_session_id}/heartbeat", + dependencies=[Depends(verify_internal_service)], +) +async def heartbeat_file_lock( + edit_session_id: str, + payload: FileLockTokenRequest, + request: Request, +) -> dict[str, Any]: + ttl_ms: int = request.app.state.file_lock_ttl_ms + now = utcnow() + async with request.app.state.session_factory() as session: + item = await owned_session( + session, + edit_session_id=edit_session_id, + workspace_id=payload.workspace_id, + user_id=payload.user_id, + ) + token_hash = verify_token(item, payload.lock_token) + if item.session_status != "active": + raise lock_error( + status.HTTP_409_CONFLICT, + "FILE_LOCK_NOT_ACTIVE", + "编辑锁已结束", + details={"session_status": item.session_status}, + ) + result = await redis_heartbeat( + request.app.state.redis_client, + key=item.redis_lock_key, + edit_session_id=item.edit_session_id, + token_hash=token_hash, + ttl_ms=ttl_ms, + ) + if result != 1: + item.session_status = "expired" + item.ended_at = now + item.end_reason = ( + "redis_lease_expired" if result == 0 else "lock_replaced" + ) + try: + await request.app.state.runtime_lifecycle.terminate_session( + item.runtime_id, + item.jupyter_session_id, + ) + except RuntimeProviderError: + logger.warning( + "failed to terminate an expired Jupyter session", + exc_info=True, + ) + await session.commit() + raise lock_error( + status.HTTP_409_CONFLICT, + "FILE_LOCK_EXPIRED", + "编辑锁已失效,请重新申请", + retryable=True, + ) + item.last_heartbeat_at = now + item.expires_at = now + timedelta(milliseconds=ttl_ms) + if item.runtime_id: + runtime_item = await session.get( + RuntimeInstances, + item.runtime_id, + ) + if runtime_item is not None: + request.app.state.runtime_lifecycle.touch(runtime_item) + await session.commit() + return {"data": session_payload(item)} + + +@app.delete( + "/internal/v1/file-locks/{edit_session_id}", + dependencies=[Depends(verify_internal_service)], +) +async def release_file_lock( + edit_session_id: str, + payload: FileLockTokenRequest, + request: Request, +) -> dict[str, Any]: + now = utcnow() + async with request.app.state.session_factory() as session: + item = await owned_session( + session, + edit_session_id=edit_session_id, + workspace_id=payload.workspace_id, + user_id=payload.user_id, + ) + token_hash = verify_token(item, payload.lock_token) + if item.session_status != "active": + return {"data": session_payload(item), "meta": {"reused": True}} + result = await redis_release( + request.app.state.redis_client, + key=item.redis_lock_key, + edit_session_id=item.edit_session_id, + token_hash=token_hash, + ) + item.ended_at = now + if result == 1: + item.session_status = "closed" + item.end_reason = "client_release" + else: + item.session_status = "expired" + item.end_reason = ( + "redis_lease_expired" if result == 0 else "lock_replaced" + ) + try: + await request.app.state.runtime_lifecycle.terminate_session( + item.runtime_id, + item.jupyter_session_id, + ) + except RuntimeProviderError: + logger.warning( + "failed to terminate a released Jupyter session", + exc_info=True, + ) + await session.commit() + return {"data": session_payload(item), "meta": {"reused": False}} + + +@app.post( + "/internal/v1/jupyter/access-tickets/{edit_session_id}", + status_code=status.HTTP_201_CREATED, + dependencies=[Depends(verify_internal_service)], +) +async def create_jupyter_access_ticket( + edit_session_id: str, + payload: FileLockTokenRequest, + request: Request, +) -> dict[str, Any]: + now = utcnow() + ttl_seconds: int = request.app.state.jupyter_ticket_ttl_seconds + async with request.app.state.session_factory() as session: + item = await owned_session( + session, + edit_session_id=edit_session_id, + workspace_id=payload.workspace_id, + user_id=payload.user_id, + ) + token_hash = verify_token(item, payload.lock_token) + if item.session_status != "active": + raise lock_error( + status.HTTP_409_CONFLICT, + "FILE_LOCK_NOT_ACTIVE", + "编辑锁已结束", + details={"session_status": item.session_status}, + ) + + current, remaining_ms = await redis_current( + request.app.state.redis_client, + item.redis_lock_key, + ) + if ( + not current + or remaining_ms <= 0 + or current.get("edit_session_id") != item.edit_session_id + or current.get("user_id") != item.user_id + or current.get("token_hash") != token_hash + ): + raise lock_error( + status.HTTP_409_CONFLICT, + "FILE_LOCK_EXPIRED", + "编辑锁已失效,请重新申请", + retryable=True, + ) + + runtime_item = await session.get(RuntimeInstances, item.runtime_id) + if ( + runtime_item is None + or runtime_item.actual_state != "running" + ): + raise lock_error( + status.HTTP_409_CONFLICT, + "RUNTIME_NOT_RUNNING", + "Jupyter Runtime 未运行", + retryable=True, + ) + workspace = await session.get(Workspaces, item.workspace_id) + storage_object = await session.get( + StorageObjects, + item.storage_object_id, + ) + if ( + workspace is None + or storage_object is None + or not storage_object.relative_path + ): + raise lock_error( + status.HTTP_409_CONFLICT, + "JUPYTER_TARGET_NOT_FOUND", + "Jupyter 目标文件不存在", + ) + + raw_ticket = secrets.token_urlsafe(32) + expires_at = now + timedelta(seconds=ttl_seconds) + ticket_data = { + "workspace_id": item.workspace_id, + "user_id": item.user_id, + "edit_session_id": item.edit_session_id, + "runtime_id": item.runtime_id, + "jupyter_session_id": item.jupyter_session_id, + "redis_lock_key": item.redis_lock_key, + "lock_token_hash": token_hash, + "expires_at": utc_iso(expires_at), + } + stored = await request.app.state.redis_client.set( + jupyter_ticket_key(raw_ticket), + json.dumps(ticket_data, separators=(",", ":")), + ex=ttl_seconds, + nx=True, + ) + if not stored: + raise lock_error( + status.HTTP_503_SERVICE_UNAVAILABLE, + "JUPYTER_TICKET_COLLISION", + "访问票据生成失败,请重试", + retryable=True, + ) + + return { + "data": { + "ticket": raw_ticket, + "edit_session_id": item.edit_session_id, + "jupyter_url": jupyter_url( + workspace.workspace_code, + storage_object.relative_path, + ), + "expires_at": utc_iso(expires_at), + "expires_in_seconds": ttl_seconds, + } + } + + +@app.get( + "/internal/v1/jupyter/authorize", + status_code=status.HTTP_204_NO_CONTENT, + dependencies=[Depends(verify_internal_service)], +) +async def authorize_jupyter_proxy( + request: Request, + jupyter_access: str | None = Cookie( + default=None, + alias="jupyter_access", + ), + x_original_uri: str = Header(alias="X-Original-URI"), +) -> Response: + original_path = unquote(urlsplit(x_original_uri).path) + if ( + not original_path.startswith("/jupyter/") + or ".." in PurePosixPath(original_path).parts + ): + raise lock_error( + status.HTTP_403_FORBIDDEN, + "JUPYTER_PATH_FORBIDDEN", + "Jupyter 代理路径不允许访问", + ) + if not jupyter_access: + raise lock_error( + status.HTTP_401_UNAUTHORIZED, + "JUPYTER_TICKET_REQUIRED", + "缺少 Jupyter 访问票据", + ) + + raw_data = await request.app.state.redis_client.get( + jupyter_ticket_key(jupyter_access) + ) + if not raw_data: + raise lock_error( + status.HTTP_401_UNAUTHORIZED, + "JUPYTER_TICKET_EXPIRED", + "Jupyter 访问票据无效或已过期", + ) + try: + ticket_data = json.loads(raw_data) + except (TypeError, ValueError): + await request.app.state.redis_client.delete( + jupyter_ticket_key(jupyter_access) + ) + raise lock_error( + status.HTTP_401_UNAUTHORIZED, + "JUPYTER_TICKET_INVALID", + "Jupyter 访问票据无效", + ) + + current, remaining_ms = await redis_current( + request.app.state.redis_client, + ticket_data["redis_lock_key"], + ) + if ( + not current + or remaining_ms <= 0 + or current.get("edit_session_id") + != ticket_data["edit_session_id"] + or current.get("user_id") != ticket_data["user_id"] + or current.get("token_hash") + != ticket_data["lock_token_hash"] + ): + await request.app.state.redis_client.delete( + jupyter_ticket_key(jupyter_access) + ) + raise lock_error( + status.HTTP_403_FORBIDDEN, + "JUPYTER_EDIT_SESSION_INACTIVE", + "编辑会话已失效", + ) + + return Response( + status_code=status.HTTP_204_NO_CONTENT, + headers={ + "Cache-Control": "no-store", + "X-Jupyter-Upstream": "http://jupyter:8888", + "X-Jupyter-Authorization": ( + f"token {request.app.state.jupyter_token}" + ), + "X-Workspace-ID": ticket_data["workspace_id"], + }, + ) + + +async def reconcile_expired_edit_sessions(app_state: Any) -> None: + while True: + try: + now = utcnow() + async with app_state.state.session_factory() as session: + items = ( + await session.scalars( + select(EditSessions) + .where( + EditSessions.session_status == "active", + EditSessions.expires_at <= now, + ) + .limit(100) + ) + ).all() + for item in items: + current, remaining_ms = await redis_current( + app_state.state.redis_client, + item.redis_lock_key, + ) + if ( + current + and current.get("edit_session_id") + == item.edit_session_id + and remaining_ms > 0 + ): + item.expires_at = now + timedelta( + milliseconds=remaining_ms + ) + continue + try: + await ( + app_state.state.runtime_lifecycle + .terminate_session( + item.runtime_id, + item.jupyter_session_id, + ) + ) + except RuntimeProviderError: + logger.warning( + "failed to terminate an abandoned session", + exc_info=True, + ) + item.session_status = "expired" + item.ended_at = now + item.end_reason = "redis_lease_expired" + await session.commit() + except asyncio.CancelledError: + raise + except Exception: + logger.exception("edit-session reconciliation failed") + await asyncio.sleep(5) + + +async def runtime_for_member( + session: AsyncSession, + *, + runtime_id: str, + workspace_id: str, + user_id: str, +) -> tuple[RuntimeInstances, Workspaces, Roles]: + _, role, workspace = await active_member( + session, + workspace_id=workspace_id, + user_id=user_id, + ) + item = await session.get(RuntimeInstances, runtime_id) + if ( + item is None + or item.workspace_id != workspace_id + ): + raise lock_error( + status.HTTP_404_NOT_FOUND, + "RUNTIME_NOT_FOUND", + "Runtime 不存在", + ) + return item, workspace, role + + +@app.post( + "/internal/v1/runtimes/ensure", + dependencies=[Depends(verify_internal_service)], +) +async def ensure_runtime( + payload: EnsureRuntimeApiRequest, + request: Request, +) -> dict[str, Any]: + async with request.app.state.session_factory() as session: + _, _, workspace = await active_member( + session, + workspace_id=payload.workspace_id, + user_id=payload.user_id, + ) + try: + item, reused = ( + await request.app.state.runtime_lifecycle.ensure_running( + session, + workspace=workspace, + user_id=payload.user_id, + request_id=payload.request_id, + ) + ) + await session.commit() + except RuntimeProviderError as exc: + await session.rollback() + raise lock_error( + status.HTTP_503_SERVICE_UNAVAILABLE, + "JUPYTER_UNAVAILABLE", + "Jupyter Runtime 暂时不可用", + retryable=True, + details={"provider_error": str(exc)}, + ) from exc + return {"data": runtime_payload(item), "meta": {"reused": reused}} + + +@app.get( + "/internal/v1/runtimes/{runtime_id}", + dependencies=[Depends(verify_internal_service)], +) +async def get_runtime( + runtime_id: str, + request: Request, + workspace_id: str = Query(min_length=26, max_length=26), + user_id: str = Query(min_length=26, max_length=26), +) -> dict[str, Any]: + async with request.app.state.session_factory() as session: + item, _, _ = await runtime_for_member( + session, + runtime_id=runtime_id, + workspace_id=workspace_id, + user_id=user_id, + ) + health = await request.app.state.runtime_lifecycle.health(item) + await session.commit() + return { + "data": { + **runtime_payload(item), + "healthy": health.healthy, + "health_detail": health.detail, + } + } + + +@app.post( + "/internal/v1/runtimes/{runtime_id}/restart", + dependencies=[Depends(verify_internal_service)], +) +async def restart_runtime( + runtime_id: str, + payload: RuntimeIdentityRequest, + request: Request, +) -> dict[str, Any]: + async with request.app.state.session_factory() as session: + item, _, _ = await runtime_for_member( + session, + runtime_id=runtime_id, + workspace_id=payload.workspace_id, + user_id=payload.user_id, + ) + try: + await request.app.state.runtime_lifecycle.restart( + session, + item, + user_id=payload.user_id, + request_id=payload.request_id, + ) + await session.commit() + except RuntimeProviderError as exc: + await session.rollback() + raise lock_error( + status.HTTP_503_SERVICE_UNAVAILABLE, + "JUPYTER_UNAVAILABLE", + "Jupyter Runtime 重启失败", + retryable=True, + details={"provider_error": str(exc)}, + ) from exc + return {"data": runtime_payload(item)} + + +@app.delete( + "/internal/v1/runtimes/{runtime_id}", + dependencies=[Depends(verify_internal_service)], +) +async def stop_runtime( + runtime_id: str, + payload: StopRuntimeApiRequest, + request: Request, +) -> dict[str, Any]: + async with request.app.state.session_factory() as session: + item, _, _ = await runtime_for_member( + session, + runtime_id=runtime_id, + workspace_id=payload.workspace_id, + user_id=payload.user_id, + ) + active_sessions = ( + await session.scalars( + select(EditSessions).where( + EditSessions.runtime_id == runtime_id, + EditSessions.session_status == "active", + ) + ) + ).all() + for edit_session in active_sessions: + await redis_release( + request.app.state.redis_client, + key=edit_session.redis_lock_key, + edit_session_id=edit_session.edit_session_id, + token_hash=edit_session.lock_token_hash.hex(), + ) + try: + await request.app.state.runtime_lifecycle.terminate_session( + runtime_id, + edit_session.jupyter_session_id, + ) + except RuntimeProviderError: + logger.warning( + "failed to terminate a session during Runtime stop", + exc_info=True, + ) + edit_session.session_status = "closed" + edit_session.ended_at = utcnow() + edit_session.end_reason = "runtime_stop" + await request.app.state.runtime_lifecycle.stop( + session, + item, + user_id=payload.user_id, + request_id=payload.request_id, + reason=payload.reason, + ) + await session.commit() + return {"data": runtime_payload(item)} + + +@app.post( + "/internal/v1/runtimes/{runtime_id}/sessions", + status_code=status.HTTP_201_CREATED, + dependencies=[Depends(verify_internal_service)], +) +async def create_runtime_session( + runtime_id: str, + payload: CreateRuntimeSessionApiRequest, + request: Request, +) -> dict[str, Any]: + async with request.app.state.session_factory() as session: + item, workspace, role = await runtime_for_member( + session, + runtime_id=runtime_id, + workspace_id=payload.workspace_id, + user_id=payload.user_id, + ) + storage_object = await editable_object( + session, + AcquireFileLockRequest( + workspace_id=payload.workspace_id, + storage_object_id=payload.storage_object_id, + user_id=payload.user_id, + request_id=payload.request_id, + ), + role, + ) + if storage_object.relative_path != payload.relative_path: + raise lock_error( + status.HTTP_409_CONFLICT, + "FILE_PATH_MISMATCH", + "文件路径与对象元数据不一致", + ) + try: + runtime_session = ( + await request.app.state.runtime_lifecycle.create_session( + item, + workspace_code=workspace.workspace_code, + relative_path=payload.relative_path, + ) + ) + await session.commit() + except RuntimeProviderError as exc: + await session.rollback() + raise lock_error( + status.HTTP_503_SERVICE_UNAVAILABLE, + "JUPYTER_SESSION_FAILED", + "Jupyter Session 创建失败", + retryable=True, + details={"provider_error": str(exc)}, + ) from exc + return { + "data": { + "runtime_id": runtime_session.runtime_id, + "session_id": runtime_session.session_id, + "jupyter_url": runtime_session.jupyter_url, + }, + "meta": {"reused": runtime_session.reused}, + } + + +@app.delete( + "/internal/v1/runtimes/{runtime_id}/sessions/{session_id}", + dependencies=[Depends(verify_internal_service)], +) +async def terminate_runtime_session( + runtime_id: str, + session_id: str, + payload: RuntimeIdentityRequest, + request: Request, +) -> dict[str, Any]: + async with request.app.state.session_factory() as session: + await runtime_for_member( + session, + runtime_id=runtime_id, + workspace_id=payload.workspace_id, + user_id=payload.user_id, + ) + try: + await request.app.state.runtime_lifecycle.terminate_session( + runtime_id, + session_id, + ) + except RuntimeProviderError as exc: + raise lock_error( + status.HTTP_503_SERVICE_UNAVAILABLE, + "JUPYTER_SESSION_TERMINATION_FAILED", + "Jupyter Session 终止失败", + retryable=True, + details={"provider_error": str(exc)}, + ) from exc + return { + "data": { + "runtime_id": runtime_id, + "session_id": session_id, + "status": "terminated", + } + } + + +@app.get("/internal/health/runtime") +async def internal_health() -> dict[str, str]: + return {"status": "ready", "service": "runtime-manager"} diff --git a/runtime/src/runtime/providers/__init__.py b/runtime/src/runtime/providers/__init__.py new file mode 100644 index 0000000..befd052 --- /dev/null +++ b/runtime/src/runtime/providers/__init__.py @@ -0,0 +1 @@ +"""Runtime provider implementations.""" diff --git a/runtime/src/runtime/providers/shared_jupyter.py b/runtime/src/runtime/providers/shared_jupyter.py new file mode 100644 index 0000000..bfd8ac3 --- /dev/null +++ b/runtime/src/runtime/providers/shared_jupyter.py @@ -0,0 +1,203 @@ +from __future__ import annotations + +import hashlib +from datetime import UTC, datetime +from pathlib import PurePosixPath +from urllib.parse import quote + +import httpx + +from contracts.runtime.runtime_adapter import ( + CreateSessionRequest, + EnsureRuntimeRequest, + RuntimeEndpoint, + RuntimeHealth, + RuntimeSession, +) + + +class RuntimeProviderError(RuntimeError): + pass + + +class SharedJupyterAdapter: + """Compose provider backed by one internal Jupyter Server. + + Runtime rows are scoped to a Workspace. Each Notebook has its own + Jupyter Session and Kernel inside that server. Replacing this class with + a Docker or Kubernetes Workspace provider does not change the contract. + """ + + provider_name = "process" + runtime_ref = "compose:jupyter" + + def __init__( + self, + client: httpx.AsyncClient, + *, + internal_url: str, + proxy_base_path: str, + ) -> None: + self.client = client + self.internal_url = internal_url.rstrip("/") + "/" + self.proxy_base_path = "/" + proxy_base_path.strip("/") + "/" + + async def _request( + self, + method: str, + path: str, + *, + payload: dict | None = None, + allow_not_found: bool = False, + ) -> httpx.Response: + try: + response = await self.client.request(method, path, json=payload) + except httpx.RequestError as exc: + raise RuntimeProviderError( + f"Jupyter request failed: {type(exc).__name__}" + ) from exc + if allow_not_found and response.status_code == 404: + return response + if response.is_error: + raise RuntimeProviderError( + f"Jupyter returned HTTP {response.status_code}" + ) + return response + + async def ensure_running( + self, + request: EnsureRuntimeRequest, + ) -> RuntimeEndpoint: + health = await self.health(request.runtime_id) + if not health.healthy: + raise RuntimeProviderError( + health.detail or "Jupyter is not healthy" + ) + return RuntimeEndpoint( + runtime_id=request.runtime_id, + runtime_type="jupyter", + provider=self.provider_name, + runtime_ref=self.runtime_ref, + internal_url=self.internal_url, + proxy_base_path=self.proxy_base_path, + ) + + async def stop(self, runtime_id: str, reason: str) -> None: + # Compose keeps the shared infrastructure process alive. Stopping a + # logical Runtime terminates its sessions and updates MySQL state. + return None + + async def restart(self, runtime_id: str) -> RuntimeEndpoint: + health = await self.health(runtime_id) + if not health.healthy: + raise RuntimeProviderError( + health.detail or "Jupyter is not healthy" + ) + return RuntimeEndpoint( + runtime_id=runtime_id, + runtime_type="jupyter", + provider=self.provider_name, + runtime_ref=self.runtime_ref, + internal_url=self.internal_url, + proxy_base_path=self.proxy_base_path, + ) + + async def health(self, runtime_id: str) -> RuntimeHealth: + try: + await self._request("GET", "api/status") + except RuntimeProviderError as exc: + return RuntimeHealth( + runtime_id=runtime_id, + healthy=False, + checked_at=datetime.now(UTC), + detail=str(exc), + ) + return RuntimeHealth( + runtime_id=runtime_id, + healthy=True, + checked_at=datetime.now(UTC), + ) + + @staticmethod + def _session_path(request: CreateSessionRequest) -> str: + relative = PurePosixPath(request.relative_path.replace("\\", "/")) + if ( + relative.is_absolute() + or not relative.parts + or any(part in {"", ".", ".."} for part in relative.parts) + ): + raise RuntimeProviderError("invalid Jupyter relative path") + return PurePosixPath( + request.workspace_code, + *relative.parts, + ).as_posix() + + async def create_session( + self, + request: CreateSessionRequest, + ) -> RuntimeSession: + session_path = self._session_path(request) + encoded_path = quote(session_path, safe="/") + await self._request("GET", f"api/contents/{encoded_path}") + if session_path.lower().endswith(".ipynb"): + jupyter_url = ( + f"{self.proxy_base_path}doc/tree/{encoded_path}" + ) + else: + jupyter_url = ( + f"{self.proxy_base_path}lab/tree/{encoded_path}" + ) + + if not session_path.lower().endswith(".ipynb"): + logical_id = hashlib.sha256( + f"{request.runtime_id}:{session_path}".encode("utf-8") + ).hexdigest()[:32] + return RuntimeSession( + runtime_id=request.runtime_id, + session_id=f"file:{logical_id}", + jupyter_url=jupyter_url, + reused=True, + ) + + sessions_response = await self._request("GET", "api/sessions") + sessions = sessions_response.json() + for item in sessions: + if item.get("path") == session_path: + return RuntimeSession( + runtime_id=request.runtime_id, + session_id=str(item["id"]), + jupyter_url=jupyter_url, + reused=True, + ) + + created = ( + await self._request( + "POST", + "api/sessions", + payload={ + "path": session_path, + "name": "", + "type": "notebook", + "kernel": {"name": "python3"}, + }, + ) + ).json() + return RuntimeSession( + runtime_id=request.runtime_id, + session_id=str(created["id"]), + jupyter_url=jupyter_url, + reused=False, + ) + + async def terminate_session( + self, + runtime_id: str, + session_id: str, + ) -> None: + if session_id.startswith("file:"): + return + await self._request( + "DELETE", + f"api/sessions/{quote(session_id, safe='')}", + allow_not_found=True, + ) diff --git a/runtime/src/runtime/redis_lock.py b/runtime/src/runtime/redis_lock.py new file mode 100644 index 0000000..2c770dc --- /dev/null +++ b/runtime/src/runtime/redis_lock.py @@ -0,0 +1,110 @@ +from __future__ import annotations + +import json +from typing import Any + +from redis.asyncio import Redis + + +HEARTBEAT_SCRIPT = """ +local raw = redis.call('GET', KEYS[1]) +if not raw then + return 0 +end +local ok, value = pcall(cjson.decode, raw) +if not ok then + return -2 +end +if value['edit_session_id'] ~= ARGV[1] + or value['token_hash'] ~= ARGV[2] then + return -1 +end +redis.call('PEXPIRE', KEYS[1], ARGV[3]) +return 1 +""" + + +RELEASE_SCRIPT = """ +local raw = redis.call('GET', KEYS[1]) +if not raw then + return 0 +end +local ok, value = pcall(cjson.decode, raw) +if not ok then + return -2 +end +if value['edit_session_id'] ~= ARGV[1] + or value['token_hash'] ~= ARGV[2] then + return -1 +end +return redis.call('DEL', KEYS[1]) +""" + + +def lock_key(workspace_id: str, storage_object_id: str) -> str: + return f"lock:file:{workspace_id}:{storage_object_id}" + + +async def acquire( + client: Redis, + *, + key: str, + value: dict[str, Any], + ttl_ms: int, +) -> bool: + encoded = json.dumps( + value, + ensure_ascii=False, + separators=(",", ":"), + sort_keys=True, + ) + return bool(await client.set(key, encoded, nx=True, px=ttl_ms)) + + +async def current(client: Redis, key: str) -> tuple[dict[str, Any] | None, int]: + raw = await client.get(key) + if raw is None: + return None, -2 + try: + value = json.loads(raw) + except (TypeError, json.JSONDecodeError): + return None, await client.pttl(key) + return value, await client.pttl(key) + + +async def heartbeat( + client: Redis, + *, + key: str, + edit_session_id: str, + token_hash: str, + ttl_ms: int, +) -> int: + return int( + await client.eval( + HEARTBEAT_SCRIPT, + 1, + key, + edit_session_id, + token_hash, + ttl_ms, + ) + ) + + +async def release( + client: Redis, + *, + key: str, + edit_session_id: str, + token_hash: str, +) -> int: + return int( + await client.eval( + RELEASE_SCRIPT, + 1, + key, + edit_session_id, + token_hash, + ) + ) diff --git a/runtime/src/runtime/runtime_lifecycle.py b/runtime/src/runtime/runtime_lifecycle.py new file mode 100644 index 0000000..7a0fdf0 --- /dev/null +++ b/runtime/src/runtime/runtime_lifecycle.py @@ -0,0 +1,308 @@ +from __future__ import annotations + +from datetime import UTC, datetime, timedelta +from typing import Any + +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from common.db.models import ( + RuntimeInstances, + WorkspaceOperations, + Workspaces, +) +from common.ids import new_ulid +from contracts.runtime.runtime_adapter import ( + CreateSessionRequest, + RuntimeAdapter, + RuntimeHealth, + RuntimeSession, + EnsureRuntimeRequest, +) + + +def utcnow() -> datetime: + return datetime.now(UTC).replace(tzinfo=None) + + +class RuntimeLifecycle: + def __init__( + self, + adapter: RuntimeAdapter, + *, + lease_seconds: int, + ) -> None: + self.adapter = adapter + self.lease_seconds = lease_seconds + + def _lease_expiry(self, now: datetime) -> datetime: + return now + timedelta(seconds=self.lease_seconds) + + async def _record_operation( + self, + session: AsyncSession, + *, + workspace_id: str, + runtime_id: str, + user_id: str, + operation_type: str, + request_id: str | None, + status: str = "succeeded", + error_code: str | None = None, + error_message: str | None = None, + ) -> None: + now = utcnow() + operation_request_id = ( + f"runtime:{operation_type}:{request_id}" + if request_id + else None + ) + if operation_request_id: + existing = await session.scalar( + select(WorkspaceOperations).where( + WorkspaceOperations.request_id == operation_request_id + ) + ) + if existing is not None: + return + session.add( + WorkspaceOperations( + operation_id=new_ulid(), + workspace_id=workspace_id, + runtime_id=runtime_id, + operation_type=operation_type, + operation_status=status, + state_version=1, + request_id=operation_request_id, + requested_by=user_id, + started_at=now, + finished_at=now, + error_code=error_code, + error_message=error_message, + ) + ) + + async def ensure_running( + self, + session: AsyncSession, + *, + workspace: Workspaces, + user_id: str, + request_id: str | None, + ) -> tuple[RuntimeInstances, bool]: + # Serializes ensure_running for one Workspace across stateless Runtime + # Manager replicas. Provider calls remain behind the adapter boundary. + await session.execute( + select(Workspaces.workspace_id) + .where(Workspaces.workspace_id == workspace.workspace_id) + .with_for_update() + ) + now = utcnow() + existing = await session.scalar( + select(RuntimeInstances) + .where( + RuntimeInstances.workspace_id == workspace.workspace_id, + RuntimeInstances.runtime_type == "jupyter", + RuntimeInstances.desired_state == "running", + RuntimeInstances.actual_state.in_( + ["provisioning", "starting", "running", "unhealthy"] + ), + ) + .order_by(RuntimeInstances.created_at.desc()) + ) + if existing is not None: + # Jupyter Server is scoped to the Workspace. The user who first + # starts it is still recorded in started_by and operation audit, + # while owner_user_id=None identifies a shared Workspace Runtime. + existing.owner_user_id = None + health = await self.adapter.health(existing.runtime_id) + if health.healthy: + existing.actual_state = "running" + existing.last_heartbeat_at = now + existing.lease_expires_at = self._lease_expiry(now) + existing.state_version += 1 + existing.error_message = None + await self._record_operation( + session, + workspace_id=workspace.workspace_id, + runtime_id=existing.runtime_id, + user_id=user_id, + operation_type="open", + request_id=request_id, + ) + return existing, True + existing.actual_state = "unhealthy" + existing.state_version += 1 + existing.error_message = health.detail + + runtime_id = new_ulid() + endpoint = await self.adapter.ensure_running( + EnsureRuntimeRequest( + runtime_id=runtime_id, + workspace_id=workspace.workspace_id, + workspace_code=workspace.workspace_code, + owner_user_id=user_id, + ) + ) + item = RuntimeInstances( + runtime_id=runtime_id, + workspace_id=workspace.workspace_id, + owner_user_id=None, + runtime_type=endpoint.runtime_type, + runtime_provider=endpoint.provider, + runtime_ref=endpoint.runtime_ref, + host_node="compose", + internal_url=endpoint.internal_url, + proxy_base_path=endpoint.proxy_base_path, + desired_state="running", + actual_state="running", + state_version=1, + started_by=user_id, + started_at=now, + last_heartbeat_at=now, + lease_expires_at=self._lease_expiry(now), + ) + session.add(item) + await session.flush() + await self._record_operation( + session, + workspace_id=workspace.workspace_id, + runtime_id=item.runtime_id, + user_id=user_id, + operation_type="start", + request_id=request_id, + ) + return item, False + + async def health( + self, + item: RuntimeInstances, + ) -> RuntimeHealth: + health = await self.adapter.health(item.runtime_id) + now = utcnow() + item.last_heartbeat_at = now + item.actual_state = "running" if health.healthy else "unhealthy" + item.error_message = health.detail + if health.healthy: + item.lease_expires_at = self._lease_expiry(now) + item.state_version += 1 + return health + + async def stop( + self, + session: AsyncSession, + item: RuntimeInstances, + *, + user_id: str, + request_id: str | None, + reason: str, + ) -> None: + if item.actual_state == "stopped": + return + item.desired_state = "stopped" + item.actual_state = "stopping" + item.state_version += 1 + await self.adapter.stop(item.runtime_id, reason) + item.actual_state = "stopped" + item.stopped_at = utcnow() + item.lease_expires_at = None + item.state_version += 1 + await self._record_operation( + session, + workspace_id=item.workspace_id, + runtime_id=item.runtime_id, + user_id=user_id, + operation_type="stop", + request_id=request_id, + ) + + async def restart( + self, + session: AsyncSession, + item: RuntimeInstances, + *, + user_id: str, + request_id: str | None, + ) -> None: + item.desired_state = "running" + item.actual_state = "starting" + item.state_version += 1 + endpoint = await self.adapter.restart(item.runtime_id) + now = utcnow() + item.runtime_ref = endpoint.runtime_ref + item.internal_url = endpoint.internal_url + item.proxy_base_path = endpoint.proxy_base_path + item.actual_state = "running" + item.started_at = item.started_at or now + item.last_heartbeat_at = now + item.lease_expires_at = self._lease_expiry(now) + item.stopped_at = None + item.error_message = None + item.state_version += 1 + await self._record_operation( + session, + workspace_id=item.workspace_id, + runtime_id=item.runtime_id, + user_id=user_id, + operation_type="restart", + request_id=request_id, + ) + + async def create_session( + self, + item: RuntimeInstances, + *, + workspace_code: str, + relative_path: str, + ) -> RuntimeSession: + result = await self.adapter.create_session( + CreateSessionRequest( + runtime_id=item.runtime_id, + workspace_code=workspace_code, + relative_path=relative_path, + ) + ) + now = utcnow() + item.last_heartbeat_at = now + item.lease_expires_at = self._lease_expiry(now) + item.state_version += 1 + return result + + def touch(self, item: RuntimeInstances) -> None: + now = utcnow() + item.last_heartbeat_at = now + item.lease_expires_at = self._lease_expiry(now) + item.state_version += 1 + + async def terminate_session( + self, + runtime_id: str | None, + session_id: str | None, + ) -> None: + if runtime_id and session_id: + await self.adapter.terminate_session(runtime_id, session_id) + +def runtime_payload(item: RuntimeInstances) -> dict[str, Any]: + def iso(value: datetime | None) -> str | None: + if value is None: + return None + return value.replace(tzinfo=UTC).isoformat().replace("+00:00", "Z") + + return { + "runtime_id": item.runtime_id, + "workspace_id": item.workspace_id, + "owner_user_id": item.owner_user_id, + "runtime_type": item.runtime_type, + "runtime_provider": item.runtime_provider, + "runtime_ref": item.runtime_ref, + "internal_url": item.internal_url, + "proxy_base_path": item.proxy_base_path, + "desired_state": item.desired_state, + "actual_state": item.actual_state, + "state_version": item.state_version, + "started_at": iso(item.started_at), + "last_heartbeat_at": iso(item.last_heartbeat_at), + "lease_expires_at": iso(item.lease_expires_at), + "stopped_at": iso(item.stopped_at), + "error_message": item.error_message, + } diff --git a/runtime/src/runtime/schemas.py b/runtime/src/runtime/schemas.py new file mode 100644 index 0000000..13bf381 --- /dev/null +++ b/runtime/src/runtime/schemas.py @@ -0,0 +1,40 @@ +from __future__ import annotations + +from pydantic import BaseModel, ConfigDict, Field + + +class StrictModel(BaseModel): + model_config = ConfigDict(extra="forbid") + + +class AcquireFileLockRequest(StrictModel): + workspace_id: str = Field(min_length=26, max_length=26) + storage_object_id: str = Field(min_length=26, max_length=26) + user_id: str = Field(min_length=26, max_length=26) + request_id: str | None = Field(default=None, min_length=1, max_length=64) + + +class FileLockTokenRequest(StrictModel): + workspace_id: str = Field(min_length=26, max_length=26) + user_id: str = Field(min_length=26, max_length=26) + lock_token: str = Field(min_length=32, max_length=256) + + +class RuntimeIdentityRequest(StrictModel): + workspace_id: str = Field(min_length=26, max_length=26) + user_id: str = Field(min_length=26, max_length=26) + request_id: str | None = Field(default=None, min_length=1, max_length=64) + + +class EnsureRuntimeApiRequest(RuntimeIdentityRequest): + pass + + +class StopRuntimeApiRequest(RuntimeIdentityRequest): + reason: str = Field(default="client_request", min_length=1, max_length=64) + + +class CreateRuntimeSessionApiRequest(RuntimeIdentityRequest): + storage_object_id: str = Field(min_length=26, max_length=26) + relative_path: str = Field(min_length=1, max_length=1024) + diff --git a/schedule/Dockerfile b/schedule/Dockerfile new file mode 100644 index 0000000..b7140f4 --- /dev/null +++ b/schedule/Dockerfile @@ -0,0 +1,12 @@ +FROM python:3.12-slim-bookworm + +ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 +WORKDIR /app +COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/uv +COPY pyproject.toml uv.lock ./ +COPY common ./common +COPY schedule ./schedule +RUN uv sync --frozen --no-dev --no-editable --package schedule + +EXPOSE 8000 +CMD ["uv", "run", "--frozen", "--package", "schedule", "uvicorn", "schedule.main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/schedule/README.md b/schedule/README.md index e69de29..631a2c5 100644 --- a/schedule/README.md +++ b/schedule/README.md @@ -0,0 +1,4 @@ +# Schedule + +独立调度执行服务。负责 Outbox/Redis Streams 轮询、DAG 节点派发、稳定版本 +执行、重试、状态推进以及日志和结果回写。 diff --git a/schedule/pyproject.toml b/schedule/pyproject.toml index b6dd556..a28b6b0 100644 --- a/schedule/pyproject.toml +++ b/schedule/pyproject.toml @@ -1,24 +1,24 @@ [project] name = "schedule" -version = "0.1.0" -description = "Add your description here" -readme = "README.md" -authors = [ - { name = "tao.chen", email = "93983997+taochen-ct@users.noreply.github.com" } -] +version = "0.2.0" requires-python = ">=3.12" -dependencies = [] - -[project.scripts] -schedule = "schedule:main" - -[[tool.uv.index]] -url = "https://pypi.tuna.tsinghua.edu.cn/simple/" -default = true +dependencies = [ + "common", + "fastapi==0.116.1", + "uvicorn[standard]==0.35.0", + "httpx==0.28.1", + "redis==5.2.1", + "nbclient==0.10.2", + "nbformat==5.10.4", + "ipykernel==6.29.5", +] [tool.uv.sources] common = { workspace = true } [build-system] requires = ["hatchling"] -build-backend = "hatchling.build" \ No newline at end of file +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["src/schedule"] diff --git a/schedule/src/schedule/__init__.py b/schedule/src/schedule/__init__.py index 7849754..2eac494 100644 --- a/schedule/src/schedule/__init__.py +++ b/schedule/src/schedule/__init__.py @@ -1,2 +1 @@ -def main() -> None: - print("Hello from schedule!") +"""Scheduler operational application.""" diff --git a/schedule/src/schedule/execution.py b/schedule/src/schedule/execution.py new file mode 100644 index 0000000..86c2313 --- /dev/null +++ b/schedule/src/schedule/execution.py @@ -0,0 +1,202 @@ +from __future__ import annotations + +import asyncio +import json +import sys +import tempfile +from dataclasses import dataclass +from pathlib import Path, PurePosixPath + + +MAX_LOG_BYTES = 4 * 1024 * 1024 + + +@dataclass(frozen=True) +class ExecutionResult: + status: str + exit_code: int | None + logs: bytes + result: bytes + result_file_name: str + result_content_type: str + error_code: str | None = None + error_message: str | None = None + + +def _limited_log(value: str) -> bytes: + encoded = value.encode("utf-8", errors="replace") + if len(encoded) <= MAX_LOG_BYTES: + return encoded or b"execution produced no console output\n" + suffix = b"\n[log truncated by scheduler worker]\n" + return encoded[: MAX_LOG_BYTES - len(suffix)] + suffix + + +async def _execute_notebook( + artifact: Path, + *, + artifact_name: str, + arguments: list[str], + workspace_root: Path, + timeout_seconds: int, +) -> ExecutionResult: + output = artifact.with_name(f"executed-{artifact_name}") + process = await asyncio.create_subprocess_exec( + sys.executable, + "-m", + "schedule.notebook_runner", + "--input", + str(artifact), + "--output", + str(output), + "--workspace", + str(workspace_root), + "--timeout", + str(max(1, timeout_seconds)), + "--arguments-json", + json.dumps(arguments, ensure_ascii=False), + cwd=str(workspace_root), + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.STDOUT, + ) + try: + stdout, _ = await asyncio.wait_for( + process.communicate(), + timeout=max(1, timeout_seconds) + 30, + ) + except TimeoutError: + process.kill() + stdout, _ = await process.communicate() + status = "timed_out" + exit_code = None + error_code = "NODE_TIMEOUT" + error_message = f"notebook exceeded timeout of {timeout_seconds} seconds" + else: + exit_code = process.returncode + status = ( + "succeeded" + if exit_code == 0 + else "timed_out" + if exit_code == 124 + else "failed" + ) + error_code = ( + None + if status == "succeeded" + else "NODE_TIMEOUT" + if status == "timed_out" + else "NOTEBOOK_EXECUTION_FAILED" + ) + error_message = ( + None + if status == "succeeded" + else "Notebook execution timed out" + if status == "timed_out" + else "Notebook execution failed; see node log" + ) + return ExecutionResult( + status=status, + exit_code=exit_code, + logs=_limited_log(stdout.decode("utf-8", errors="replace")), + result=output.read_bytes() if output.is_file() else artifact.read_bytes(), + result_file_name=f"executed-{artifact_name}", + result_content_type="application/x-ipynb+json", + error_code=error_code, + error_message=error_message, + ) + + +async def _execute_python( + artifact: Path, + *, + arguments: list[str], + workspace_root: Path, + timeout_seconds: int, +) -> ExecutionResult: + process = await asyncio.create_subprocess_exec( + sys.executable, + str(artifact), + *arguments, + cwd=str(workspace_root), + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.STDOUT, + ) + try: + stdout, _ = await asyncio.wait_for( + process.communicate(), + timeout=max(1, timeout_seconds), + ) + except TimeoutError: + process.kill() + stdout, _ = await process.communicate() + message = f"node exceeded timeout of {timeout_seconds} seconds" + result = json.dumps( + {"status": "timed_out", "exit_code": None, "message": message}, + ensure_ascii=False, + ).encode("utf-8") + return ExecutionResult( + status="timed_out", + exit_code=None, + logs=_limited_log( + stdout.decode("utf-8", errors="replace") + f"\n{message}\n" + ), + result=result, + result_file_name=f"{artifact.stem}-result.json", + result_content_type="application/json", + error_code="NODE_TIMEOUT", + error_message=message, + ) + exit_code = process.returncode + status = "succeeded" if exit_code == 0 else "failed" + message = None if status == "succeeded" else f"process exited with code {exit_code}" + result = json.dumps( + {"status": status, "exit_code": exit_code}, + ensure_ascii=False, + ).encode("utf-8") + return ExecutionResult( + status=status, + exit_code=exit_code, + logs=_limited_log(stdout.decode("utf-8", errors="replace")), + result=result, + result_file_name=f"{artifact.stem}-result.json", + result_content_type="application/json", + error_code=None if status == "succeeded" else "PROCESS_EXIT_NONZERO", + error_message=message, + ) + + +async def execute_artifact( + source: bytes, + *, + run_id: str, + node_run_id: str, + script_type: str, + artifact_path: str, + arguments: list[str], + workspace_root: Path, + timeout_seconds: int, +) -> ExecutionResult: + runtime_root = workspace_root / "runtime_tmp" / "schedule-runs" + runtime_root.mkdir(parents=True, exist_ok=True) + suffix = ".ipynb" if script_type == "notebook" else ".py" + raw_name = PurePosixPath(artifact_path.replace("\\", "/")).name + artifact_name = raw_name if raw_name.endswith(suffix) else f"artifact{suffix}" + prefix = f"{run_id[-6:]}-{node_run_id[-6:]}-" + with tempfile.TemporaryDirectory(prefix=prefix, dir=runtime_root) as directory: + artifact = Path(directory) / artifact_name + artifact.write_bytes(source) + if script_type == "notebook": + return await _execute_notebook( + artifact, + artifact_name=artifact_name, + arguments=arguments, + workspace_root=workspace_root, + timeout_seconds=timeout_seconds, + ) + if script_type == "python": + return await _execute_python( + artifact, + arguments=arguments, + workspace_root=workspace_root, + timeout_seconds=timeout_seconds, + ) + raise ValueError(f"unsupported script_type: {script_type}") diff --git a/schedule/src/schedule/main.py b/schedule/src/schedule/main.py index f0ff52a..a5e4478 100644 --- a/schedule/src/schedule/main.py +++ b/schedule/src/schedule/main.py @@ -1,5 +1,51 @@ -# coding=utf-8 -""" -@Time :2026/7/29 -@Author :tao.chen -""" +from __future__ import annotations + +import os +from contextlib import asynccontextmanager +from pathlib import Path +from typing import Any, AsyncIterator + +from common.db import create_database_engine, create_session_factory +from common.service_app import create_service_app +from schedule.service import ( + SchedulerService, + build_object_store, + build_redis_client, + build_storage_http_client, +) +from schedule.storage_client import SchedulerStorageClient + + +@asynccontextmanager +async def lifespan(app: Any) -> AsyncIterator[None]: + engine = create_database_engine(os.environ["DATABASE_URL"]) + session_factory = create_session_factory(engine) + redis = build_redis_client() + storage_http_client = build_storage_http_client() + service = SchedulerService( + session_factory=session_factory, + redis=redis, + object_store=build_object_store(), + storage_client=SchedulerStorageClient( + storage_http_client, + os.environ["INTERNAL_SERVICE_TOKEN"], + ), + workspace_root=Path( + os.getenv("WORKSPACE_ROOT", "/workspace/workspaces") + ), + ) + app.state.scheduler_service = service + await service.start() + try: + yield + finally: + await service.close() + await storage_http_client.aclose() + await redis.aclose() + await engine.dispose() + + +app = create_service_app( + os.getenv("SERVICE_NAME", "scheduler-worker"), + lifespan=lifespan, +) diff --git a/schedule/src/schedule/notebook_runner.py b/schedule/src/schedule/notebook_runner.py new file mode 100644 index 0000000..dbe7882 --- /dev/null +++ b/schedule/src/schedule/notebook_runner.py @@ -0,0 +1,83 @@ +from __future__ import annotations + +import argparse +import json +import sys +import traceback +from pathlib import Path + +import nbformat +from nbclient import NotebookClient + + +def emit_outputs(notebook: object) -> None: + for cell in notebook.cells: # type: ignore[attr-defined] + if cell.get("cell_type") != "code": + continue + for output in cell.get("outputs", []): + output_type = output.get("output_type") + if output_type == "stream": + text = output.get("text", "") + print( + "".join(text) if isinstance(text, list) else str(text), + end="", + flush=True, + ) + elif output_type == "error": + print( + f"{output.get('ename', 'Error')}: " + f"{output.get('evalue', '')}", + file=sys.stderr, + flush=True, + ) + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--input", required=True) + parser.add_argument("--output", required=True) + parser.add_argument("--workspace", required=True) + parser.add_argument("--timeout", required=True, type=int) + parser.add_argument("--arguments-json", default="[]") + args = parser.parse_args() + + source = Path(args.input) + output = Path(args.output) + workspace = Path(args.workspace) + arguments = json.loads(args.arguments_json) + if not isinstance(arguments, list) or not all( + isinstance(item, str) for item in arguments + ): + raise ValueError("arguments-json must contain an array of strings") + + notebook = nbformat.read(source, as_version=4) + if arguments: + notebook.cells.insert( + 0, + nbformat.v4.new_code_cell( + "import sys\n" + f"sys.argv = {json.dumps([source.name, *arguments], ensure_ascii=False)}", + metadata={"tags": ["injected-parameters"]}, + ), + ) + exit_code = 0 + try: + client = NotebookClient( + notebook, + timeout=max(1, args.timeout), + kernel_name="python3", + allow_errors=False, + ) + client.execute(cwd=str(workspace)) + except Exception as exc: + traceback.print_exc() + exit_code = 124 if "timeout" in type(exc).__name__.lower() else 1 + finally: + output.parent.mkdir(parents=True, exist_ok=True) + nbformat.write(notebook, output) + emit_outputs(notebook) + raise SystemExit(exit_code) + + +if __name__ == "__main__": + main() diff --git a/schedule/src/schedule/service.py b/schedule/src/schedule/service.py new file mode 100644 index 0000000..20b5a3a --- /dev/null +++ b/schedule/src/schedule/service.py @@ -0,0 +1,903 @@ +from __future__ import annotations + +import asyncio +import hashlib +import json +import logging +import os +import socket +import traceback +from collections.abc import Awaitable, Callable +from contextlib import suppress +from datetime import timedelta +from pathlib import Path +from typing import Any + +import boto3 +import httpx +from redis.asyncio import Redis +from redis.exceptions import ResponseError +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker + +from common.db.models import ( + ConsumerInbox, + OutboxEvents, + ScheduleNodeRuns, + ScheduleRuns, + Schedules, + StorageObjects, + Versions, + Workspaces, +) +from common.eventing import ( + STREAM_BY_EVENT_TYPE, + add_outbox_event, + event_time, + utcnow, +) +from common.ids import new_ulid +from common.db.session import session_scope +from schedule.execution import ExecutionResult, execute_artifact +from schedule.storage_client import SchedulerStorageClient + + +LOGGER = logging.getLogger(__name__) +TERMINAL_NODE_STATES = { + "succeeded", + "failed", + "skipped", + "cancelled", + "timed_out", +} +FAILED_NODE_STATES = {"failed", "cancelled", "timed_out"} +TERMINAL_RUN_STATES = { + "succeeded", + "failed", + "cancelled", + "timed_out", +} + + +class SchedulerService: + def __init__( + self, + *, + session_factory: async_sessionmaker[AsyncSession], + redis: Redis, + object_store: Any, + storage_client: SchedulerStorageClient, + workspace_root: Path, + ) -> None: + self.session_factory = session_factory + self.redis = redis + self.object_store = object_store + self.storage_client = storage_client + self.workspace_root = workspace_root + self.consumer_name = ( + os.getenv("SCHEDULER_CONSUMER_NAME") + or f"{socket.gethostname()}-{os.getpid()}" + ) + self.tasks: list[asyncio.Task[Any]] = [] + + async def start(self) -> None: + await self._ensure_group( + "stream:scheduler:commands", + "schedule-orchestrator", + ) + await self._ensure_group("stream:jobs:execute", "job-workers") + await self._ensure_group("stream:jobs:results", "schedule-results") + self.tasks = [ + asyncio.create_task( + self._outbox_loop(), + name="scheduler-outbox-publisher", + ), + asyncio.create_task( + self._consumer_loop( + "stream:scheduler:commands", + "schedule-orchestrator", + self._handle_run_requested, + ), + name="schedule-orchestrator", + ), + asyncio.create_task( + self._consumer_loop( + "stream:jobs:execute", + "job-workers", + self._handle_node_execute, + ), + name="job-worker", + ), + asyncio.create_task( + self._consumer_loop( + "stream:jobs:results", + "schedule-results", + self._handle_node_finished, + ), + name="schedule-results", + ), + ] + + async def close(self) -> None: + for task in self.tasks: + task.cancel() + for task in self.tasks: + with suppress(asyncio.CancelledError): + await task + self.tasks.clear() + + async def _ensure_group(self, stream: str, group: str) -> None: + try: + await self.redis.xgroup_create( + stream, + group, + id="0-0", + mkstream=True, + ) + except ResponseError as exc: + if "BUSYGROUP" not in str(exc): + raise + + async def _outbox_loop(self) -> None: + while True: + try: + published = await self._publish_outbox_batch() + if not published: + await asyncio.sleep(0.35) + except asyncio.CancelledError: + raise + except Exception: + LOGGER.exception("outbox publisher iteration failed") + await asyncio.sleep(1) + + async def _publish_outbox_batch(self) -> int: + now = utcnow() + async with session_scope(self.session_factory) as session: + events = list( + ( + await session.scalars( + select(OutboxEvents) + .where( + OutboxEvents.event_status == "pending", + OutboxEvents.available_at <= now, + ) + .order_by(OutboxEvents.created_at) + .limit(20) + .with_for_update(skip_locked=True) + ) + ).all() + ) + for item in events: + stream = STREAM_BY_EVENT_TYPE.get(item.event_type) + if stream is None: + item.event_status = "failed" + item.last_error = f"unsupported event type: {item.event_type}" + continue + try: + await self.redis.xadd( + stream, + { + "event": json.dumps( + item.payload_json, + ensure_ascii=False, + separators=(",", ":"), + ) + }, + ) + item.event_status = "published" + item.published_at = utcnow() + item.last_error = None + except Exception as exc: + item.retry_count += 1 + item.last_error = str(exc)[:2000] + raise + return len(events) + + async def _consumer_loop( + self, + stream: str, + group: str, + handler: Callable[[dict[str, Any], str], Awaitable[None]], + ) -> None: + while True: + try: + messages = await self.redis.xreadgroup( + group, + self.consumer_name, + {stream: ">"}, + count=5, + block=1000, + ) + entries: list[tuple[str, dict[str, str]]] = [] + for _, stream_messages in messages: + entries.extend(stream_messages) + if not entries: + claimed = await self.redis.xautoclaim( + stream, + group, + self.consumer_name, + min_idle_time=10_000, + start_id="0-0", + count=5, + ) + if len(claimed) >= 2: + entries.extend(claimed[1]) + for message_id, fields in entries: + try: + raw = fields.get("event") + if not raw: + raise ValueError("stream message has no event field") + event = json.loads(raw) + except asyncio.CancelledError: + raise + except (ValueError, TypeError, json.JSONDecodeError): + LOGGER.exception( + "discarding malformed message %s from %s", + message_id, + stream, + ) + await self.redis.xack(stream, group, message_id) + continue + try: + await handler(event, message_id) + except asyncio.CancelledError: + raise + except Exception: + LOGGER.exception( + "consumer %s failed for message %s", + group, + message_id, + ) + continue + await self.redis.xack(stream, group, message_id) + except asyncio.CancelledError: + raise + except Exception: + LOGGER.exception("consumer loop %s failed", group) + await asyncio.sleep(1) + + async def _start_inbox( + self, + session: AsyncSession, + *, + consumer_name: str, + event_id: str, + message_id: str, + ) -> tuple[ConsumerInbox, bool]: + item = await session.get( + ConsumerInbox, + (consumer_name, event_id), + with_for_update=True, + ) + if item is not None and item.process_status == "succeeded": + return item, False + if item is None: + item = ConsumerInbox( + consumer_name=consumer_name, + event_id=event_id, + process_status="processing", + message_id=message_id, + ) + session.add(item) + else: + item.process_status = "processing" + item.message_id = message_id + item.error_message = None + return item, True + + @staticmethod + def _finish_inbox(item: ConsumerInbox) -> None: + item.process_status = "succeeded" + item.processed_at = utcnow() + item.error_message = None + + async def _handle_run_requested( + self, + event: dict[str, Any], + message_id: str, + ) -> None: + if event.get("event_type") != "schedule.run.requested": + raise ValueError("unexpected event type") + payload = event["payload"] + async with session_scope(self.session_factory) as session: + inbox, should_process = await self._start_inbox( + session, + consumer_name="schedule-orchestrator", + event_id=event["event_id"], + message_id=message_id, + ) + if not should_process: + return + run = await session.scalar( + select(ScheduleRuns) + .where(ScheduleRuns.run_id == payload["run_id"]) + .with_for_update() + ) + if run is None: + raise ValueError("schedule run does not exist") + if run.run_status not in TERMINAL_RUN_STATES: + if run.run_status == "queued": + run.run_status = "running" + run.started_at = utcnow() + run.state_version += 1 + await self._advance_run( + session, + run, + trace_id=event["trace_id"], + ) + self._finish_inbox(inbox) + + async def _dispatch_node( + self, + session: AsyncSession, + *, + run: ScheduleRuns, + node: dict[str, Any], + attempt_no: int, + trace_id: str, + delay_seconds: int = 0, + ) -> ScheduleNodeRuns: + node_run = ScheduleNodeRuns( + node_run_id=new_ulid(), + run_id=run.run_id, + node_id=node["node_id"], + versions_id=node["versions_id"], + attempt_no=attempt_no, + node_status="queued", + state_version=0, + message=( + f"等待重试({delay_seconds} 秒)" + if delay_seconds + else "等待 Worker 执行" + ), + ) + session.add(node_run) + await add_outbox_event( + session, + event_type="job.node.execute", + producer="schedule-orchestrator", + trace_id=trace_id, + aggregate_type="schedule_node_run", + aggregate_id=node_run.node_run_id, + idempotency_key=f"{node_run.node_run_id}:{attempt_no}", + available_at=( + utcnow() + timedelta(seconds=delay_seconds) + if delay_seconds + else None + ), + payload={ + "workspace_id": run.workspace_id, + "run_id": run.run_id, + "node_run_id": node_run.node_run_id, + "node_id": node["node_id"], + "versions_id": node["versions_id"], + "attempt_no": attempt_no, + "script_type": node["script_type"], + "artifact_object_id": node["artifact_object_id"], + "artifact_path": node["artifact_path"], + "timeout_seconds": node["timeout_seconds"], + "arguments": node.get("arguments", []), + }, + ) + return node_run + + async def _advance_run( + self, + session: AsyncSession, + run: ScheduleRuns, + *, + trace_id: str, + ) -> None: + snapshot = run.schedule_snapshot + nodes = snapshot.get("nodes", []) + node_by_id = {node["node_id"]: node for node in nodes} + parents: dict[str, set[str]] = {node_id: set() for node_id in node_by_id} + for edge in snapshot.get("edges", []): + parents.setdefault(edge["target_node_id"], set()).add( + edge["source_node_id"] + ) + rows = list( + ( + await session.scalars( + select(ScheduleNodeRuns) + .where(ScheduleNodeRuns.run_id == run.run_id) + .order_by(ScheduleNodeRuns.attempt_no) + ) + ).all() + ) + latest: dict[str, ScheduleNodeRuns] = {} + for row in rows: + current = latest.get(row.node_id) + if current is None or row.attempt_no >= current.attempt_no: + latest[row.node_id] = row + + max_concurrency = max(1, int(snapshot.get("max_concurrency", 1))) + while True: + changed = False + active_count = sum( + item.node_status in {"queued", "running"} + for item in latest.values() + ) + for node in nodes: + current = latest.get(node["node_id"]) + if ( + current is not None + and current.node_status in FAILED_NODE_STATES + and current.attempt_no <= int(node.get("retry_count", 0)) + and active_count < max_concurrency + ): + retried = await self._dispatch_node( + session, + run=run, + node=node, + attempt_no=current.attempt_no + 1, + trace_id=trace_id, + delay_seconds=int(node.get("retry_interval_sec", 0)), + ) + latest[node["node_id"]] = retried + active_count += 1 + changed = True + + exhausted_failure = any( + item.node_status in FAILED_NODE_STATES + and item.attempt_no + > int(node_by_id[item.node_id].get("retry_count", 0)) + for item in latest.values() + ) + stop_all = ( + snapshot.get("failure_policy", "stop") == "stop" + and exhausted_failure + ) + for node in nodes: + node_id = node["node_id"] + if node_id in latest: + continue + parent_runs = [latest.get(parent) for parent in parents[node_id]] + parent_failed = any( + item is not None + and item.node_status in TERMINAL_NODE_STATES + and item.node_status != "succeeded" + for item in parent_runs + ) + if stop_all or parent_failed: + skipped = ScheduleNodeRuns( + node_run_id=new_ulid(), + run_id=run.run_id, + node_id=node_id, + versions_id=node["versions_id"], + attempt_no=1, + node_status="skipped", + state_version=1, + finished_at=utcnow(), + duration_ms=0, + message=( + "调度失败策略为 stop,未再启动" + if stop_all + else "上游节点未成功,已跳过" + ), + ) + session.add(skipped) + latest[node_id] = skipped + changed = True + elif ( + all( + item is not None and item.node_status == "succeeded" + for item in parent_runs + ) + and active_count < max_concurrency + ): + dispatched = await self._dispatch_node( + session, + run=run, + node=node, + attempt_no=1, + trace_id=trace_id, + ) + latest[node_id] = dispatched + active_count += 1 + changed = True + if not changed: + break + + if nodes and len(latest) == len(nodes) and all( + item.node_status in TERMINAL_NODE_STATES + for item in latest.values() + ): + now = utcnow() + succeeded = all( + item.node_status == "succeeded" for item in latest.values() + ) + run.run_status = "succeeded" if succeeded else "failed" + run.error_code = None if succeeded else "SCHEDULE_NODE_FAILED" + run.error_message = ( + None if succeeded else "one or more schedule nodes did not succeed" + ) + run.finished_at = now + if run.started_at: + run.duration_ms = max( + 0, + int((now - run.started_at).total_seconds() * 1000), + ) + run.state_version += 1 + + async def _execution_context( + self, + payload: dict[str, Any], + ) -> dict[str, Any]: + async with self.session_factory() as session: + row = ( + await session.execute( + select( + ScheduleNodeRuns, + ScheduleRuns, + Versions, + StorageObjects, + Workspaces, + Schedules, + ) + .join( + ScheduleRuns, + ScheduleRuns.run_id == ScheduleNodeRuns.run_id, + ) + .join( + Versions, + Versions.versions_id == ScheduleNodeRuns.versions_id, + ) + .join( + StorageObjects, + StorageObjects.storage_object_id + == Versions.artifact_object_id, + ) + .join( + Workspaces, + Workspaces.workspace_id == ScheduleRuns.workspace_id, + ) + .join( + Schedules, + Schedules.schedule_id == ScheduleRuns.schedule_id, + ) + .where( + ScheduleNodeRuns.node_run_id == payload["node_run_id"], + ) + ) + ).one_or_none() + if row is None: + raise ValueError("node execution metadata not found") + node_run, run, version, storage, workspace, schedule = row + if storage.object_status != "available": + raise ValueError("stable version artifact is not available") + if storage.storage_backend != "rustfs": + raise ValueError("stable version artifact is not stored in RustFS") + if not storage.bucket_name or not storage.object_key: + raise ValueError("stable version artifact location is incomplete") + user_id = run.triggered_by or schedule.created_by + return { + "node_status": node_run.node_status, + "workspace_id": run.workspace_id, + "workspace_code": workspace.workspace_code, + "user_id": user_id, + "bucket_name": storage.bucket_name, + "object_key": storage.object_key, + "content_hash": version.content_hash, + } + + async def _fallback_execution_context( + self, + payload: dict[str, Any], + ) -> dict[str, Any]: + async with self.session_factory() as session: + row = ( + await session.execute( + select(ScheduleRuns, Workspaces, Schedules) + .join( + Workspaces, + Workspaces.workspace_id == ScheduleRuns.workspace_id, + ) + .join( + Schedules, + Schedules.schedule_id == ScheduleRuns.schedule_id, + ) + .where(ScheduleRuns.run_id == payload["run_id"]) + ) + ).one_or_none() + if row is None: + raise ValueError("schedule run execution context not found") + run, workspace, schedule = row + return { + "workspace_id": run.workspace_id, + "workspace_code": workspace.workspace_code, + "user_id": run.triggered_by or schedule.created_by, + } + + async def _download_artifact( + self, + *, + bucket_name: str, + object_key: str, + content_hash: str, + ) -> bytes: + def read() -> bytes: + response = self.object_store.get_object( + Bucket=bucket_name, + Key=object_key, + ) + body = response["Body"] + try: + return body.read() + finally: + body.close() + + content = await asyncio.to_thread(read) + if hashlib.sha256(content).hexdigest() != content_hash: + raise ValueError("stable version artifact hash mismatch") + return content + + async def _set_node_running( + self, + event: dict[str, Any], + message_id: str, + ) -> bool: + payload = event["payload"] + async with session_scope(self.session_factory) as session: + inbox, should_process = await self._start_inbox( + session, + consumer_name="job-workers", + event_id=event["event_id"], + message_id=message_id, + ) + if not should_process: + return False + node_run = await session.scalar( + select(ScheduleNodeRuns) + .where( + ScheduleNodeRuns.node_run_id == payload["node_run_id"], + ) + .with_for_update() + ) + if node_run is None: + raise ValueError("schedule node run does not exist") + if node_run.node_status in TERMINAL_NODE_STATES: + self._finish_inbox(inbox) + return False + if node_run.node_status == "queued": + node_run.node_status = "running" + node_run.started_at = utcnow() + node_run.message = "Worker 正在执行稳定版本" + node_run.state_version += 1 + return True + + async def _upload_execution_artifacts( + self, + *, + payload: dict[str, Any], + context: dict[str, Any], + result: ExecutionResult, + ) -> tuple[str | None, str | None, str | None]: + log_id: str | None = None + result_id: str | None = None + upload_error: str | None = None + try: + log_object = await self.storage_client.create_object( + workspace_id=context["workspace_id"], + user_id=context["user_id"], + usage_type="run_log", + file_name=f"{payload['node_run_id']}.log", + content_type="text/plain; charset=utf-8", + content=result.logs, + idempotency_key=f"{payload['node_run_id']}:log", + ) + log_id = log_object["storage_object_id"] + result_object = await self.storage_client.create_object( + workspace_id=context["workspace_id"], + user_id=context["user_id"], + usage_type="run_result", + file_name=result.result_file_name, + content_type=result.result_content_type, + content=result.result, + idempotency_key=f"{payload['node_run_id']}:result", + ) + result_id = result_object["storage_object_id"] + except Exception as exc: + upload_error = f"result upload failed: {exc}"[:2000] + LOGGER.exception("failed to upload node execution artifacts") + return log_id, result_id, upload_error + + async def _handle_node_execute( + self, + event: dict[str, Any], + message_id: str, + ) -> None: + if event.get("event_type") != "job.node.execute": + raise ValueError("unexpected event type") + payload = event["payload"] + if not await self._set_node_running(event, message_id): + return + started_at = utcnow() + context: dict[str, Any] | None = None + try: + context = await self._execution_context(payload) + content = await self._download_artifact( + bucket_name=context["bucket_name"], + object_key=context["object_key"], + content_hash=context["content_hash"], + ) + workspace_root = ( + self.workspace_root / context["workspace_code"] + ).resolve() + workspace_root.mkdir(parents=True, exist_ok=True) + result = await execute_artifact( + content, + run_id=payload["run_id"], + node_run_id=payload["node_run_id"], + script_type=payload["script_type"], + artifact_path=payload["artifact_path"], + arguments=[str(item) for item in payload.get("arguments", [])], + workspace_root=workspace_root, + timeout_seconds=int(payload["timeout_seconds"]), + ) + except Exception as exc: + trace = traceback.format_exc() + result = ExecutionResult( + status="failed", + exit_code=1, + logs=trace.encode("utf-8", errors="replace"), + result=json.dumps( + {"status": "failed", "error": str(exc)}, + ensure_ascii=False, + ).encode("utf-8"), + result_file_name=f"{payload['node_run_id']}-result.json", + result_content_type="application/json", + error_code="WORKER_EXECUTION_FAILED", + error_message=str(exc)[:2000], + ) + if context is None: + context = await self._fallback_execution_context(payload) + + log_id, result_id, upload_error = await self._upload_execution_artifacts( + payload=payload, + context=context, + result=result, + ) + finished_at = utcnow() + duration_ms = max( + 0, + int((finished_at - started_at).total_seconds() * 1000), + ) + error_message = result.error_message + if upload_error: + error_message = ( + f"{error_message}; {upload_error}" + if error_message + else upload_error + )[:2000] + final_status = "failed" if upload_error else result.status + final_error_code = ( + "ARTIFACT_UPLOAD_FAILED" if upload_error else result.error_code + ) + + async with session_scope(self.session_factory) as session: + node_run = await session.scalar( + select(ScheduleNodeRuns) + .where( + ScheduleNodeRuns.node_run_id == payload["node_run_id"], + ) + .with_for_update() + ) + if node_run is None: + raise ValueError("schedule node run disappeared") + inbox = await session.get( + ConsumerInbox, + ("job-workers", event["event_id"]), + with_for_update=True, + ) + if inbox is None: + raise ValueError("job worker inbox record disappeared") + if node_run.node_status not in TERMINAL_NODE_STATES: + node_run.node_status = final_status + node_run.finished_at = finished_at + node_run.duration_ms = duration_ms + node_run.exit_code = result.exit_code + node_run.message = ( + "节点执行成功" + if final_status == "succeeded" + else (error_message or "节点执行失败") + )[:2000] + node_run.metrics_json = { + "log_size_bytes": len(result.logs), + "result_size_bytes": len(result.result), + } + node_run.logs_object_id = log_id + node_run.result_object_id = result_id + node_run.state_version += 1 + await add_outbox_event( + session, + event_type="job.node.finished", + producer="job-worker", + trace_id=event["trace_id"], + aggregate_type="schedule_node_run", + aggregate_id=node_run.node_run_id, + idempotency_key=( + f"{node_run.node_run_id}:{node_run.attempt_no}:finished" + ), + payload={ + "workspace_id": context["workspace_id"], + "run_id": node_run.run_id, + "node_run_id": node_run.node_run_id, + "node_id": node_run.node_id, + "versions_id": node_run.versions_id, + "attempt_no": node_run.attempt_no, + "node_status": node_run.node_status, + "exit_code": node_run.exit_code, + "started_at": event_time( + node_run.started_at or started_at + ), + "finished_at": event_time(finished_at), + "duration_ms": duration_ms, + "logs_object_id": log_id, + "result_object_id": result_id, + "error_code": final_error_code, + "error_message": error_message, + }, + ) + self._finish_inbox(inbox) + + async def _handle_node_finished( + self, + event: dict[str, Any], + message_id: str, + ) -> None: + if event.get("event_type") != "job.node.finished": + raise ValueError("unexpected event type") + payload = event["payload"] + async with session_scope(self.session_factory) as session: + inbox, should_process = await self._start_inbox( + session, + consumer_name="schedule-results", + event_id=event["event_id"], + message_id=message_id, + ) + if not should_process: + return + run = await session.scalar( + select(ScheduleRuns) + .where(ScheduleRuns.run_id == payload["run_id"]) + .with_for_update() + ) + if run is None: + raise ValueError("schedule run does not exist") + if run.run_status not in TERMINAL_RUN_STATES: + await self._advance_run( + session, + run, + trace_id=event["trace_id"], + ) + self._finish_inbox(inbox) + + +def build_redis_client() -> Redis: + return Redis( + host=os.getenv("REDIS_HOST", "redis"), + port=int(os.getenv("REDIS_PORT", "6379")), + password=os.getenv("REDIS_PASSWORD") or None, + decode_responses=True, + ) + + +def build_object_store() -> Any: + return boto3.client( + "s3", + endpoint_url=os.getenv( + "RUSTFS_INTERNAL_ENDPOINT", + "http://rustfs:9000", + ), + aws_access_key_id=os.environ["RUSTFS_ACCESS_KEY"], + aws_secret_access_key=os.environ["RUSTFS_SECRET_KEY"], + region_name="us-east-1", + ) + + +def build_storage_http_client() -> httpx.AsyncClient: + return httpx.AsyncClient( + base_url=os.getenv("STORAGE_API_URL", "http://storage_api:8000"), + timeout=httpx.Timeout(60.0), + ) diff --git a/schedule/src/schedule/storage_client.py b/schedule/src/schedule/storage_client.py new file mode 100644 index 0000000..3177b5a --- /dev/null +++ b/schedule/src/schedule/storage_client.py @@ -0,0 +1,41 @@ +from __future__ import annotations + +import base64 +from typing import Any + +import httpx + + +class SchedulerStorageClient: + def __init__(self, client: httpx.AsyncClient, service_token: str) -> None: + self.client = client + self.headers = {"X-Service-Token": service_token} + + async def create_object( + self, + *, + workspace_id: str, + user_id: str, + usage_type: str, + file_name: str, + content_type: str, + content: bytes, + idempotency_key: str, + ) -> dict[str, Any]: + response = await self.client.post( + "/internal/v1/objects", + headers=self.headers, + json={ + "workspace_id": workspace_id, + "user_id": user_id, + "usage_type": usage_type, + "file_name": file_name, + "content_type": content_type, + "content_base64": base64.b64encode(content).decode("ascii"), + "visibility": "workspace", + "is_immutable": True, + "idempotency_key": idempotency_key, + }, + ) + response.raise_for_status() + return response.json()["data"] diff --git a/uv.lock b/uv.lock index a9356eb..884ed62 100644 --- a/uv.lock +++ b/uv.lock @@ -18,621 +18,551 @@ members = [ [[package]] name = "alembic" version = "1.18.5" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" } +source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mako" }, { name = "sqlalchemy" }, { name = "typing-extensions" }, ] -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/1a/cc/ac0bed8e562e7407fe55c3ba85a4dce86e6dbd8730887bd1e406a6c5c18a/alembic-1.18.5.tar.gz", hash = "sha256:1554982221dd17e9a749b53902407578eb305e453f71999e8c7f0a48389fff8e", size = 2060480, upload-time = "2026-06-25T15:20:54.888Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1a/cc/ac0bed8e562e7407fe55c3ba85a4dce86e6dbd8730887bd1e406a6c5c18a/alembic-1.18.5.tar.gz", hash = "sha256:1554982221dd17e9a749b53902407578eb305e453f71999e8c7f0a48389fff8e", size = 2060480, upload-time = "2026-06-25T15:20:54.888Z" } wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/96/78/5fe6dc3a3a5b2f5a2a4faef8bfe336d5fa049a38884ab3172e0098160c01/alembic-1.18.5-py3-none-any.whl", hash = "sha256:06d8ba9d04558022f5395e9317de03d270f3dced49cee01f89fe7a13c26f14bc", size = 264664, upload-time = "2026-06-25T15:20:56.673Z" }, -] - -[[package]] -name = "annotated-doc" -version = "0.0.4" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" } -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" } -wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, + { url = "https://files.pythonhosted.org/packages/96/78/5fe6dc3a3a5b2f5a2a4faef8bfe336d5fa049a38884ab3172e0098160c01/alembic-1.18.5-py3-none-any.whl", hash = "sha256:06d8ba9d04558022f5395e9317de03d270f3dced49cee01f89fe7a13c26f14bc", size = 264664, upload-time = "2026-06-25T15:20:56.673Z" }, ] [[package]] name = "annotated-types" version = "0.8.0" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" } -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/5f/56/a8120250d128bed162cd73c76d45f6ef9991f3e068f62a8ee060afa3104a/annotated_types-0.8.0.tar.gz", hash = "sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7", size = 15893, upload-time = "2026-07-23T20:16:13.995Z" } +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/56/a8120250d128bed162cd73c76d45f6ef9991f3e068f62a8ee060afa3104a/annotated_types-0.8.0.tar.gz", hash = "sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7", size = 15893, upload-time = "2026-07-23T20:16:13.995Z" } wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/99/91/8acff4f5e50511b911bbccb72b8628a49c68ce14148cd9f6431094859a90/annotated_types-0.8.0-py3-none-any.whl", hash = "sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0", size = 13427, upload-time = "2026-07-23T20:16:12.938Z" }, + { url = "https://files.pythonhosted.org/packages/99/91/8acff4f5e50511b911bbccb72b8628a49c68ce14148cd9f6431094859a90/annotated_types-0.8.0-py3-none-any.whl", hash = "sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0", size = 13427, upload-time = "2026-07-23T20:16:12.938Z" }, ] [[package]] name = "anyio" version = "4.14.2" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" } +source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "idna" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/61/cc/a381afa6efea9f496eff839d4a6a1aed3bfafc7b3ab4b0d1b243a12573dd/anyio-4.14.2.tar.gz", hash = "sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f", size = 260176, upload-time = "2026-07-12T20:29:07.082Z" } +sdist = { url = "https://files.pythonhosted.org/packages/61/cc/a381afa6efea9f496eff839d4a6a1aed3bfafc7b3ab4b0d1b243a12573dd/anyio-4.14.2.tar.gz", hash = "sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f", size = 260176, upload-time = "2026-07-12T20:29:07.082Z" } wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl", hash = "sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494", size = 125813, upload-time = "2026-07-12T20:29:05.763Z" }, + { url = "https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl", hash = "sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494", size = 125813, upload-time = "2026-07-12T20:29:05.763Z" }, ] [[package]] name = "appnope" version = "0.1.4" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" } -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/35/5d/752690df9ef5b76e169e68d6a129fa6d08a7100ca7f754c89495db3c6019/appnope-0.1.4.tar.gz", hash = "sha256:1de3860566df9caf38f01f86f65e0e13e379af54f9e4bee1e66b48f2efffd1ee", size = 4170, upload-time = "2024-02-06T09:43:11.258Z" } +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/35/5d/752690df9ef5b76e169e68d6a129fa6d08a7100ca7f754c89495db3c6019/appnope-0.1.4.tar.gz", hash = "sha256:1de3860566df9caf38f01f86f65e0e13e379af54f9e4bee1e66b48f2efffd1ee", size = 4170, upload-time = "2024-02-06T09:43:11.258Z" } wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/81/29/5ecc3a15d5a33e31b26c11426c45c501e439cb865d0bff96315d86443b78/appnope-0.1.4-py2.py3-none-any.whl", hash = "sha256:502575ee11cd7a28c0205f379b525beefebab9d161b7c964670864014ed7213c", size = 4321, upload-time = "2024-02-06T09:43:09.663Z" }, -] - -[[package]] -name = "argon2-cffi" -version = "25.1.0" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" } -dependencies = [ - { name = "argon2-cffi-bindings" }, -] -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/0e/89/ce5af8a7d472a67cc819d5d998aa8c82c5d860608c4db9f46f1162d7dab9/argon2_cffi-25.1.0.tar.gz", hash = "sha256:694ae5cc8a42f4c4e2bf2ca0e64e51e23a040c6a517a85074683d3959e1346c1", size = 45706, upload-time = "2025-06-03T06:55:32.073Z" } -wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/4f/d3/a8b22fa575b297cd6e3e3b0155c7e25db170edf1c74783d6a31a2490b8d9/argon2_cffi-25.1.0-py3-none-any.whl", hash = "sha256:fdc8b074db390fccb6eb4a3604ae7231f219aa669a2652e0f20e16ba513d5741", size = 14657, upload-time = "2025-06-03T06:55:30.804Z" }, -] - -[[package]] -name = "argon2-cffi-bindings" -version = "25.1.0" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" } -dependencies = [ - { name = "cffi" }, -] -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/5c/2d/db8af0df73c1cf454f71b2bbe5e356b8c1f8041c979f505b3d3186e520a9/argon2_cffi_bindings-25.1.0.tar.gz", hash = "sha256:b957f3e6ea4d55d820e40ff76f450952807013d361a65d7f28acc0acbf29229d", size = 1783441, upload-time = "2025-07-30T10:02:05.147Z" } -wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/60/97/3c0a35f46e52108d4707c44b95cfe2afcafc50800b5450c197454569b776/argon2_cffi_bindings-25.1.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:3d3f05610594151994ca9ccb3c771115bdb4daef161976a266f0dd8aa9996b8f", size = 54393, upload-time = "2025-07-30T10:01:40.97Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/9d/f4/98bbd6ee89febd4f212696f13c03ca302b8552e7dbf9c8efa11ea4a388c3/argon2_cffi_bindings-25.1.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8b8efee945193e667a396cbc7b4fb7d357297d6234d30a489905d96caabde56b", size = 29328, upload-time = "2025-07-30T10:01:41.916Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/43/24/90a01c0ef12ac91a6be05969f29944643bc1e5e461155ae6559befa8f00b/argon2_cffi_bindings-25.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3c6702abc36bf3ccba3f802b799505def420a1b7039862014a65db3205967f5a", size = 31269, upload-time = "2025-07-30T10:01:42.716Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d4/d3/942aa10782b2697eee7af5e12eeff5ebb325ccfb86dd8abda54174e377e4/argon2_cffi_bindings-25.1.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a1c70058c6ab1e352304ac7e3b52554daadacd8d453c1752e547c76e9c99ac44", size = 86558, upload-time = "2025-07-30T10:01:43.943Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/0d/82/b484f702fec5536e71836fc2dbc8c5267b3f6e78d2d539b4eaa6f0db8bf8/argon2_cffi_bindings-25.1.0-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e2fd3bfbff3c5d74fef31a722f729bf93500910db650c925c2d6ef879a7e51cb", size = 92364, upload-time = "2025-07-30T10:01:44.887Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c9/c1/a606ff83b3f1735f3759ad0f2cd9e038a0ad11a3de3b6c673aa41c24bb7b/argon2_cffi_bindings-25.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c4f9665de60b1b0e99bcd6be4f17d90339698ce954cfd8d9cf4f91c995165a92", size = 85637, upload-time = "2025-07-30T10:01:46.225Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/44/b4/678503f12aceb0262f84fa201f6027ed77d71c5019ae03b399b97caa2f19/argon2_cffi_bindings-25.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ba92837e4a9aa6a508c8d2d7883ed5a8f6c308c89a4790e1e447a220deb79a85", size = 91934, upload-time = "2025-07-30T10:01:47.203Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f0/c7/f36bd08ef9bd9f0a9cff9428406651f5937ce27b6c5b07b92d41f91ae541/argon2_cffi_bindings-25.1.0-cp314-cp314t-win32.whl", hash = "sha256:84a461d4d84ae1295871329b346a97f68eade8c53b6ed9a7ca2d7467f3c8ff6f", size = 28158, upload-time = "2025-07-30T10:01:48.341Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b3/80/0106a7448abb24a2c467bf7d527fe5413b7fdfa4ad6d6a96a43a62ef3988/argon2_cffi_bindings-25.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b55aec3565b65f56455eebc9b9f34130440404f27fe21c3b375bf1ea4d8fbae6", size = 32597, upload-time = "2025-07-30T10:01:49.112Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/05/b8/d663c9caea07e9180b2cb662772865230715cbd573ba3b5e81793d580316/argon2_cffi_bindings-25.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:87c33a52407e4c41f3b70a9c2d3f6056d88b10dad7695be708c5021673f55623", size = 28231, upload-time = "2025-07-30T10:01:49.92Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/1d/57/96b8b9f93166147826da5f90376e784a10582dd39a393c99bb62cfcf52f0/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:aecba1723ae35330a008418a91ea6cfcedf6d31e5fbaa056a166462ff066d500", size = 54121, upload-time = "2025-07-30T10:01:50.815Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/0a/08/a9bebdb2e0e602dde230bdde8021b29f71f7841bd54801bcfd514acb5dcf/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:2630b6240b495dfab90aebe159ff784d08ea999aa4b0d17efa734055a07d2f44", size = 29177, upload-time = "2025-07-30T10:01:51.681Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b6/02/d297943bcacf05e4f2a94ab6f462831dc20158614e5d067c35d4e63b9acb/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:7aef0c91e2c0fbca6fc68e7555aa60ef7008a739cbe045541e438373bc54d2b0", size = 31090, upload-time = "2025-07-30T10:01:53.184Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c1/93/44365f3d75053e53893ec6d733e4a5e3147502663554b4d864587c7828a7/argon2_cffi_bindings-25.1.0-cp39-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e021e87faa76ae0d413b619fe2b65ab9a037f24c60a1e6cc43457ae20de6dc6", size = 81246, upload-time = "2025-07-30T10:01:54.145Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/09/52/94108adfdd6e2ddf58be64f959a0b9c7d4ef2fa71086c38356d22dc501ea/argon2_cffi_bindings-25.1.0-cp39-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d3e924cfc503018a714f94a49a149fdc0b644eaead5d1f089330399134fa028a", size = 87126, upload-time = "2025-07-30T10:01:55.074Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/72/70/7a2993a12b0ffa2a9271259b79cc616e2389ed1a4d93842fac5a1f923ffd/argon2_cffi_bindings-25.1.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c87b72589133f0346a1cb8d5ecca4b933e3c9b64656c9d175270a000e73b288d", size = 80343, upload-time = "2025-07-30T10:01:56.007Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/78/9a/4e5157d893ffc712b74dbd868c7f62365618266982b64accab26bab01edc/argon2_cffi_bindings-25.1.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1db89609c06afa1a214a69a462ea741cf735b29a57530478c06eb81dd403de99", size = 86777, upload-time = "2025-07-30T10:01:56.943Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/74/cd/15777dfde1c29d96de7f18edf4cc94c385646852e7c7b0320aa91ccca583/argon2_cffi_bindings-25.1.0-cp39-abi3-win32.whl", hash = "sha256:473bcb5f82924b1becbb637b63303ec8d10e84c8d241119419897a26116515d2", size = 27180, upload-time = "2025-07-30T10:01:57.759Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e2/c6/a759ece8f1829d1f162261226fbfd2c6832b3ff7657384045286d2afa384/argon2_cffi_bindings-25.1.0-cp39-abi3-win_amd64.whl", hash = "sha256:a98cd7d17e9f7ce244c0803cad3c23a7d379c301ba618a5fa76a67d116618b98", size = 31715, upload-time = "2025-07-30T10:01:58.56Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/42/b9/f8d6fa329ab25128b7e98fd83a3cb34d9db5b059a9847eddb840a0af45dd/argon2_cffi_bindings-25.1.0-cp39-abi3-win_arm64.whl", hash = "sha256:b0fdbcf513833809c882823f98dc2f931cf659d9a1429616ac3adebb49f5db94", size = 27149, upload-time = "2025-07-30T10:01:59.329Z" }, -] - -[[package]] -name = "arrow" -version = "1.4.0" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" } -dependencies = [ - { name = "python-dateutil" }, - { name = "tzdata" }, -] -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b9/33/032cdc44182491aa708d06a68b62434140d8c50820a087fac7af37703357/arrow-1.4.0.tar.gz", hash = "sha256:ed0cc050e98001b8779e84d461b0098c4ac597e88704a655582b21d116e526d7", size = 152931, upload-time = "2025-10-18T17:46:46.761Z" } -wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ed/c9/d7977eaacb9df673210491da99e6a247e93df98c715fc43fd136ce1d3d33/arrow-1.4.0-py3-none-any.whl", hash = "sha256:749f0769958ebdc79c173ff0b0670d59051a535fa26e8eba02953dc19eb43205", size = 68797, upload-time = "2025-10-18T17:46:45.663Z" }, + { url = "https://files.pythonhosted.org/packages/81/29/5ecc3a15d5a33e31b26c11426c45c501e439cb865d0bff96315d86443b78/appnope-0.1.4-py2.py3-none-any.whl", hash = "sha256:502575ee11cd7a28c0205f379b525beefebab9d161b7c964670864014ed7213c", size = 4321, upload-time = "2024-02-06T09:43:09.663Z" }, ] [[package]] name = "asttokens" version = "3.0.2" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" } -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/25/1e/faf0f247f6f881b98fc4d6d07e14085cb89d13665084e6d6ac1dc2c03d0b/asttokens-3.0.2.tar.gz", hash = "sha256:3ecdbd8f2cc195f53ccada3a613538bb5f9ef6f6869129f13e03c30a677b8fe2", size = 63136, upload-time = "2026-07-12T03:31:49.084Z" } +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/25/1e/faf0f247f6f881b98fc4d6d07e14085cb89d13665084e6d6ac1dc2c03d0b/asttokens-3.0.2.tar.gz", hash = "sha256:3ecdbd8f2cc195f53ccada3a613538bb5f9ef6f6869129f13e03c30a677b8fe2", size = 63136, upload-time = "2026-07-12T03:31:49.084Z" } wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d4/2b/04b8a15f3a1c77bc79ddf5c73875327f34b4fa75982df2b76e45e402d364/asttokens-3.0.2-py3-none-any.whl", hash = "sha256:9da13157f5b28becde0bd374fc677dcd3c290614264eff096f167c469cd9f933", size = 28702, upload-time = "2026-07-12T03:31:47.542Z" }, + { url = "https://files.pythonhosted.org/packages/d4/2b/04b8a15f3a1c77bc79ddf5c73875327f34b4fa75982df2b76e45e402d364/asttokens-3.0.2-py3-none-any.whl", hash = "sha256:9da13157f5b28becde0bd374fc677dcd3c290614264eff096f167c469cd9f933", size = 28702, upload-time = "2026-07-12T03:31:47.542Z" }, ] [[package]] -name = "async-lru" -version = "2.3.0" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" } -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e8/1f/989ecfef8e64109a489fff357450cb73fa73a865a92bd8c272170a6922c2/async_lru-2.3.0.tar.gz", hash = "sha256:89bdb258a0140d7313cf8f4031d816a042202faa61d0ab310a0a538baa1c24b6", size = 16332, upload-time = "2026-03-19T01:04:32.413Z" } +name = "asyncmy" +version = "0.2.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/78/3c/d8297584c40f3d1af55365026bcdca7844ecfea1d917ad19df48f8331a26/asyncmy-0.2.11.tar.gz", hash = "sha256:c3d65d959dde62c911e39ecd1ad0f1339a5e6929fc411d48cfc2f82846190bf4", size = 62865, upload-time = "2026-01-15T11:32:30.368Z" } wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e5/e2/c2e3abf398f80732e58b03be77bde9022550d221dd8781bf586bd4d97cc1/async_lru-2.3.0-py3-none-any.whl", hash = "sha256:eea27b01841909316f2cc739807acea1c623df2be8c5cfad7583286397bb8315", size = 8403, upload-time = "2026-03-19T01:04:30.883Z" }, + { url = "https://files.pythonhosted.org/packages/ca/93/3b4c7f9b35a27e80bd2f305c4c8d7ae56b6dd40d616d34dd4dbb818c90c9/asyncmy-0.2.11-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:914dddd2ba884822a304297f6ca96026548a100339a4e0ca5c427f6cfa3e4b62", size = 1731817, upload-time = "2026-01-15T11:31:55.486Z" }, + { url = "https://files.pythonhosted.org/packages/7b/65/c70b2b8d014b21504de7e2027e2456f7774cec855766ec1808da47d70b24/asyncmy-0.2.11-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5ea7ca4bcf7747b1fd4e80f16aa461972e0931cecd3bc43a3ba8e14a5d368a98", size = 1712017, upload-time = "2026-01-15T11:31:56.795Z" }, + { url = "https://files.pythonhosted.org/packages/65/a9/f326999a1ffacc7738376fa68c7ede164db9e5520bb4dbd35f1fdd5704dd/asyncmy-0.2.11-cp312-cp312-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:b2fd6e04efca56d9176e7b620ee47dd1dd334e2eb03c1ae1580954c2625b99d1", size = 4986605, upload-time = "2026-01-15T11:31:58.462Z" }, + { url = "https://files.pythonhosted.org/packages/b5/f2/634326efc5fca15620eb106194d2287997a31625dc95f1940a3cef2f80a7/asyncmy-0.2.11-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4df583dd9d09e817c4cc68b706133d5da453faf56da431a24fcdd64b9552e26b", size = 5226256, upload-time = "2026-01-15T11:32:00.095Z" }, + { url = "https://files.pythonhosted.org/packages/35/7f/4ecd2dcee1d13d49a301ab8ee11a33c75ded4b3089bdb7bf5fb385ed162d/asyncmy-0.2.11-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b2d473b683db7fa1acb167a4bc25ea38e398c991427bd8aa708a9b75059a1d33", size = 5045263, upload-time = "2026-01-15T11:32:01.653Z" }, + { url = "https://files.pythonhosted.org/packages/fe/3e/d94fc4a0ca1e2e492982db607c26b48a95ba668e24755f3bf00da68ae9be/asyncmy-0.2.11-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d03803975e5dfe74ef4af18411da3716681fde78a65a8758237e7e3e4a342ba1", size = 5194397, upload-time = "2026-01-15T11:32:03.068Z" }, + { url = "https://files.pythonhosted.org/packages/1e/06/d1bb47ce9ed32ba02f2ff44118d5eb36702d38f97cd824bd6a51d3decfdf/asyncmy-0.2.11-cp312-cp312-win32.whl", hash = "sha256:564fc38b3a0665663b8b2ec35fc34fd2768688ba5c869a0d8bd1eb57b85351b2", size = 1560945, upload-time = "2026-01-15T11:32:04.543Z" }, + { url = "https://files.pythonhosted.org/packages/74/d8/973c576c84f4b706a45372c959778feca6842033ccbbd26b2bfe344ebc4b/asyncmy-0.2.11-cp312-cp312-win_amd64.whl", hash = "sha256:84e23466602407da7e126fb5f2da2948c69a6f0d40d4ea7771331771e05c1c2e", size = 1637442, upload-time = "2026-01-15T11:32:05.802Z" }, + { url = "https://files.pythonhosted.org/packages/83/9a/b5b77690f7287acb0a284319e85378c6f4063cd3617dd5311e00f332d628/asyncmy-0.2.11-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0a48be02bdae5e5324ac2d142d7afc6dd9c6af546fd892804c9d8e58d8107980", size = 1727740, upload-time = "2026-01-15T11:32:07.443Z" }, + { url = "https://files.pythonhosted.org/packages/10/28/7b168dc84704edb0b60f7906bfba3a451fd90c0cb2443edbb377b1a11d20/asyncmy-0.2.11-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:babed99ea1cf7edb476dba23c27560b2a042de46e61678c0cfa3bc017e5f49e4", size = 1706138, upload-time = "2026-01-15T11:32:08.898Z" }, + { url = "https://files.pythonhosted.org/packages/ec/27/ac7363e8ab95f2048852851bbbef12d4eee62363d202d7e566291023ece4/asyncmy-0.2.11-cp313-cp313-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:709cc8147edad072b5176d87878a985323c87cc017c460073414f2b7d5ae9d01", size = 4942591, upload-time = "2026-01-15T11:32:10.127Z" }, + { url = "https://files.pythonhosted.org/packages/08/81/092314cc97e3732535804f2d3e1b966daeaa3a33a8e9a686328cf09498ad/asyncmy-0.2.11-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:373aecf8cd17662c13bab69dc36db7242be8e956242164469b8733886fb2ec0a", size = 5178039, upload-time = "2026-01-15T11:32:12.088Z" }, + { url = "https://files.pythonhosted.org/packages/2a/9b/b884404bac62d9b6efbc9006c4b80ad55e8b0bb6f585b44eee1eceb07b1c/asyncmy-0.2.11-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e23ea478e6638e479dfab2674d2c39a21160c7d750d5c8cf2a0e205d947a63b7", size = 4987628, upload-time = "2026-01-15T11:32:13.979Z" }, + { url = "https://files.pythonhosted.org/packages/00/65/68e576aecd2a43d383123e3a66339e6a3535495b0e81443e374a3d3c356d/asyncmy-0.2.11-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:577272e238aff9b985eb880b49b1ba009e1fd1133b754fc71c833ab5bd9561ee", size = 5143375, upload-time = "2026-01-15T11:32:15.38Z" }, + { url = "https://files.pythonhosted.org/packages/4e/e4/cd30ea75ab96e5c6fe0daf6bd1871753fe5a1677515530fa0bc1a807dd6c/asyncmy-0.2.11-cp313-cp313-win32.whl", hash = "sha256:29536a08bf8c96437188ae4080fdd09c5a82cbe93794d0996cd0dd238f632664", size = 1559106, upload-time = "2026-01-15T11:32:16.895Z" }, + { url = "https://files.pythonhosted.org/packages/0c/3e/497e3ac839d7d18e79770b977f90e6f17a87181f95b8aed59359ff4aba0c/asyncmy-0.2.11-cp313-cp313-win_amd64.whl", hash = "sha256:f095af7b980505158609ca0bcdd0d14d1e48893e43fc1856c7cecfd9439f498c", size = 1635619, upload-time = "2026-01-15T11:32:18.241Z" }, ] [[package]] name = "attrs" version = "26.1.0" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" } -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" } +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" } wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, -] - -[[package]] -name = "babel" -version = "2.18.0" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" } -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/7d/b2/51899539b6ceeeb420d40ed3cd4b7a40519404f9baf3d4ac99dc413a834b/babel-2.18.0.tar.gz", hash = "sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d", size = 9959554, upload-time = "2026-02-01T12:30:56.078Z" } -wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl", hash = "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35", size = 10196845, upload-time = "2026-02-01T12:30:53.445Z" }, + { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, ] [[package]] name = "backend" -version = "0.1.0" +version = "0.2.0" source = { editable = "backend" } dependencies = [ - { name = "boto3" }, + { name = "alembic" }, + { name = "common" }, + { name = "croniter" }, + { name = "cryptography" }, { name = "fastapi" }, { name = "httpx" }, - { name = "loguru" }, - { name = "pydantic" }, - { name = "uvicorn" }, + { name = "uvicorn", extra = ["standard"] }, ] [package.metadata] requires-dist = [ - { name = "boto3", specifier = ">=1.43.57" }, - { name = "fastapi", specifier = ">=0.140.0" }, - { name = "httpx", specifier = ">=0.28.1" }, - { name = "loguru", specifier = ">=0.7.3" }, - { name = "pydantic", specifier = ">=2.13.4" }, - { name = "uvicorn", specifier = ">=0.51.0" }, -] - -[[package]] -name = "beautifulsoup4" -version = "4.15.0" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" } -dependencies = [ - { name = "soupsieve" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/43/65/318323f98dbee45d42dff61d8f047181bc6f2268a9068cfad035a46be5af/beautifulsoup4-4.15.0.tar.gz", hash = "sha256:288e3ca7d54b06f2ac191970bc275c1939cb46d450b255bf6718b04aa37ab4f7", size = 632571, upload-time = "2026-06-07T16:44:20.453Z" } -wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/88/c6/92fcd42f1ba33e1184263f25bfabf3d27c383410470f169e4b8163bf9c17/beautifulsoup4-4.15.0-py3-none-any.whl", hash = "sha256:d6f88de62e1d4e38ecb1077eb9724cd0eff29d2a08ca16a401e9b9e93f117cf9", size = 109924, upload-time = "2026-06-07T16:44:21.566Z" }, -] - -[[package]] -name = "bleach" -version = "6.4.0" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" } -dependencies = [ - { name = "webencodings" }, -] -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/48/3c/e12ac860709702bd5ebeb9b56a4fe334f1001246ee1b8f2b7ee28912df7d/bleach-6.4.0.tar.gz", hash = "sha256:4202482733d85cedd04e59fcb2f89f4e4c7c385a78d3c3c23c30446843a37452", size = 204857, upload-time = "2026-06-05T13:01:13.734Z" } -wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/58/9d/40b6267367182187139a4000b82a3b287d84d745bccd808e75d916920e9d/bleach-6.4.0-py3-none-any.whl", hash = "sha256:4b6b6a54fff2e69a3dde9d21cc6301220bee3c3cb792187d11403fd795031081", size = 165109, upload-time = "2026-06-05T13:01:12.504Z" }, -] - -[package.optional-dependencies] -css = [ - { name = "tinycss2" }, + { name = "alembic", specifier = "==1.18.5" }, + { name = "common", editable = "common" }, + { name = "croniter", specifier = "==6.2.4" }, + { name = "cryptography", specifier = "==49.0.0" }, + { name = "fastapi", specifier = "==0.116.1" }, + { name = "httpx", specifier = "==0.28.1" }, + { name = "uvicorn", extras = ["standard"], specifier = "==0.35.0" }, ] [[package]] name = "boto3" version = "1.43.57" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" } +source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "botocore" }, { name = "jmespath" }, { name = "s3transfer" }, ] -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c7/30/324319a914752e4021358ccf8252c04364eb8358f9d41d9c3f021959b363/boto3-1.43.57.tar.gz", hash = "sha256:549c95e45f9b04cf0c69727632dbdda23b84dcd3d0ed7981c6aaff72ef9cb5af", size = 112690, upload-time = "2026-07-27T19:31:09.32Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/30/324319a914752e4021358ccf8252c04364eb8358f9d41d9c3f021959b363/boto3-1.43.57.tar.gz", hash = "sha256:549c95e45f9b04cf0c69727632dbdda23b84dcd3d0ed7981c6aaff72ef9cb5af", size = 112690, upload-time = "2026-07-27T19:31:09.32Z" } wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/80/a4/2c4ee25556a997a81ea01073962cf8351da3707412c584d053b3861d09e8/boto3-1.43.57-py3-none-any.whl", hash = "sha256:115ed9cac409d9b57e2437b52fdb4286660d12a2bd44a0f48d530e68623d09a7", size = 140028, upload-time = "2026-07-27T19:31:07.226Z" }, + { url = "https://files.pythonhosted.org/packages/80/a4/2c4ee25556a997a81ea01073962cf8351da3707412c584d053b3861d09e8/boto3-1.43.57-py3-none-any.whl", hash = "sha256:115ed9cac409d9b57e2437b52fdb4286660d12a2bd44a0f48d530e68623d09a7", size = 140028, upload-time = "2026-07-27T19:31:07.226Z" }, ] [[package]] name = "botocore" version = "1.43.57" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" } +source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jmespath" }, { name = "python-dateutil" }, { name = "urllib3" }, ] -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/0b/92/1f8a454cf90bcb0c0e32a25817441cb7f3702fdd76710d7018abad12a2fc/botocore-1.43.57.tar.gz", hash = "sha256:001a5653bebc03b862bde2da63bad4adb0b30072a3f247aba4daae5aa097b546", size = 15739550, upload-time = "2026-07-27T19:30:57.916Z" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/92/1f8a454cf90bcb0c0e32a25817441cb7f3702fdd76710d7018abad12a2fc/botocore-1.43.57.tar.gz", hash = "sha256:001a5653bebc03b862bde2da63bad4adb0b30072a3f247aba4daae5aa097b546", size = 15739550, upload-time = "2026-07-27T19:30:57.916Z" } wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/41/7a/1cfe9c296df0fa6e0143c8622b10f21fa8fb9cce25450e9a8e5cf6523e4b/botocore-1.43.57-py3-none-any.whl", hash = "sha256:319c6d79f66c3f3f2b538f7dcdc90e410a15a7bfced1db06479134947e629482", size = 15424433, upload-time = "2026-07-27T19:30:55.08Z" }, + { url = "https://files.pythonhosted.org/packages/41/7a/1cfe9c296df0fa6e0143c8622b10f21fa8fb9cce25450e9a8e5cf6523e4b/botocore-1.43.57-py3-none-any.whl", hash = "sha256:319c6d79f66c3f3f2b538f7dcdc90e410a15a7bfced1db06479134947e629482", size = 15424433, upload-time = "2026-07-27T19:30:55.08Z" }, ] [[package]] name = "certifi" version = "2026.7.22" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" } -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a3/c2/24167ea9858356b47a87a50d39908bfdb72ceeefe0041586e704e5376b3a/certifi-2026.7.22.tar.gz", hash = "sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55", size = 138112, upload-time = "2026-07-22T03:35:12.644Z" } +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/c2/24167ea9858356b47a87a50d39908bfdb72ceeefe0041586e704e5376b3a/certifi-2026.7.22.tar.gz", hash = "sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55", size = 138112, upload-time = "2026-07-22T03:35:12.644Z" } wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/0b/a7/71ac2cff56fec219ed242bb11b8efb69fcc4bec75db06fb7bfe35de520e6/certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775", size = 136983, upload-time = "2026-07-22T03:35:11.276Z" }, + { url = "https://files.pythonhosted.org/packages/0b/a7/71ac2cff56fec219ed242bb11b8efb69fcc4bec75db06fb7bfe35de520e6/certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775", size = 136983, upload-time = "2026-07-22T03:35:11.276Z" }, ] [[package]] name = "cffi" version = "2.1.0" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" } +source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pycparser", marker = "implementation_name != 'PyPy'" }, ] -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/57/5f/ff100cae70ebe9d8df1c01a00e510e45d9adb5c1fdda84791b199141de97/cffi-2.1.0.tar.gz", hash = "sha256:efc1cdd798b1aaf39b4610bba7aad28c9bea9b910f25c784ccf9ec1fa719d1f9", size = 531036, upload-time = "2026-07-06T21:34:30.382Z" } +sdist = { url = "https://files.pythonhosted.org/packages/57/5f/ff100cae70ebe9d8df1c01a00e510e45d9adb5c1fdda84791b199141de97/cffi-2.1.0.tar.gz", hash = "sha256:efc1cdd798b1aaf39b4610bba7aad28c9bea9b910f25c784ccf9ec1fa719d1f9", size = 531036, upload-time = "2026-07-06T21:34:30.382Z" } wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/1e/85/990925db5df586ec90beb97529c853497e7f85ba0234830447faf41c3057/cffi-2.1.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:df2b82571a1b30f58a87bf4e5a9e78d2b1eff6c6ce8fd3aa3757221f93f0863f", size = 184829, upload-time = "2026-07-06T21:32:44.324Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/4b/92/e7bb136ad6b5352603732cf907ef862ca103f20f2031c1735a46300c20c9/cffi-2.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:78474632761faa0fb96f30b1c928c84ebcf68713cbb80d15bab09dfe61640fde", size = 184728, upload-time = "2026-07-06T21:32:45.683Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c3/c0/d1ec30ffb370f748f2fb54425972bfef9871e0132e82fb589c46b6676049/cffi-2.1.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:5972433ad71a9e46516584ef60a0fda12d9dc459938d1539c3ddecf9bdc1368d", size = 214815, upload-time = "2026-07-06T21:32:48.557Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/1b/dc/5620cf930688be01f2d673804291de757a934c90b946dbdc3d84130c2ea4/cffi-2.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b6422532152adf4e59b110cb2808cee7a033800952f5c036b4af047ee43199e7", size = 222429, upload-time = "2026-07-06T21:32:49.848Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/4b/a4/77b53abbf7a1e0beb9637edbef2a94d15f9c822f591e85d439ffd91519a6/cffi-2.1.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:46b1c8db8f6122420f32d02fffb924c2fe9bc772d228c7c711748fff56aabb2b", size = 210315, upload-time = "2026-07-06T21:32:51.221Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/58/0c/f528df19cc94b675087324d4760d9e6d5bfae97d6217aa4fac43de4f5fcc/cffi-2.1.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9fafc5aa2e2a39aaf7f8cc0c1f044a9b07fca12e558dca53a3cc5c654ad67a7", size = 208859, upload-time = "2026-07-06T21:32:52.512Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/62/f2/c9522a81c32132799a1972c39f5c5f8b4c8b9f00488a23feaa6c06f07741/cffi-2.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1e9f50d192a3e525b15a75ab5114e442d83d657b7ec29182a991bc9a88fd3a66", size = 221844, upload-time = "2026-07-06T21:32:53.704Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/6e/28/bd53988b9833e8f8ad539d26f4c07a6b3f6bcb1e9e02e7ca038250b3428d/cffi-2.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:98fff996e983a36d3aa2eca83af40c5821202e7e6f32d13ae94e3d2286f10cfe", size = 225287, upload-time = "2026-07-06T21:32:54.907Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/79/99/0d0fd37f055224085f42bbb2c022d002e17dde4a97972822327b07d84101/cffi-2.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:379de10ce1ba048b1448599d1b37b24caee16309d1ac98d3982fc997f768700b", size = 223681, upload-time = "2026-07-06T21:32:56.329Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b0/80/c138990aa2a70b1a269f6e06348729836d733d6f970867943f61d367f8cc/cffi-2.1.0-cp312-cp312-win32.whl", hash = "sha256:9b8f0f26ca4e7513c534d351eca551947d053fac438f2a04ac96d882909b0d3a", size = 175269, upload-time = "2026-07-06T21:32:57.777Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a8/eb/f636456ff21a83fc13c032b58cc5dde061691546ac79efa284b2989b7982/cffi-2.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:c97f080ea627e2863524c5af3836e2270b5f5dfff1f104392b959f8df0c5d384", size = 185881, upload-time = "2026-07-06T21:32:59.253Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/dd/2c/400ea43e721727dca8a65c4521390e9196757caba4a45643acb2b63271b8/cffi-2.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:6d194185eabd279f1c05ebe3504265ddfc5ad2b58d0714f7db9f01da592e9eb6", size = 180088, upload-time = "2026-07-06T21:33:02.278Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/96/88/a996879e2eeccb815f6e3a5967b12a308257412acec882039d386bd2aa7b/cffi-2.1.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:10537b1df4967ca26d21e5072d7d54188354483b91dc75058968d3f0cf13fbda", size = 194331, upload-time = "2026-07-06T21:33:03.697Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/58/85/7ae00d5c8dd6266f4e944c3db630f3c5c9a98b61d469c714d848b1d8138a/cffi-2.1.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:a95b05f9baf29b91171b3a8bd2020b028835243e7b0ff6bb23e2a3c228518b1b", size = 196966, upload-time = "2026-07-06T21:33:05.353Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/8c/e9/45c3a76ad8d43ad9261f4c95436da61128d3ca545d72b9612c0ab5be0b1c/cffi-2.1.0-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:15faec4adfff450819f3aee0e2e02c812de6edb88203aa58807955db2003472a", size = 184795, upload-time = "2026-07-06T21:33:06.699Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/84/4c/82f132cb4418ee6d953d982b19191e87e2a6372c8a4ce36e50b69d6ade4a/cffi-2.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:716ff8ec22f20b4d988b12884086bcef0fc99737043e503f7a3935a6be99b1ea", size = 184746, upload-time = "2026-07-06T21:33:08.071Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a0/1c/4ed5a0e5bdca6cbc275556de3328dd1b76fd0c11cc13c88fe66d1d8715f2/cffi-2.1.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:63960549e4f8dc41e31accb97b975abaecfc44c03e396c093a6436763c2ea7db", size = 214747, upload-time = "2026-07-06T21:33:09.671Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/3a/a6/e879bb68cc23a2bc9ba8f4b7d8019f0c2694bad2ab6c4a3701d429439f58/cffi-2.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ff067a8d8d880e7809e4ac88eb009bb848870115317b306666502ccad30b147f", size = 222392, upload-time = "2026-07-06T21:33:10.896Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/88/f6/01890cfd63c08f8eb96a8319b0443690197d240a8bd6346048cf7bde9190/cffi-2.1.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:3b926723c13eba9f81d2ef3820d63aeceec3b2d4639906047bf675cb8a7a500d", size = 210285, upload-time = "2026-07-06T21:33:12.251Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a6/cf/2b684132056f438567b61e19d690dd31cd0921ace051e0a458be6074369e/cffi-2.1.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:47ff3a8bfd8cb9da1af7524b965127095055654c177fcfc7578debcb015eecd0", size = 208801, upload-time = "2026-07-06T21:33:13.617Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/6f/08/f2e7d62c460faae0926f2d6e423694aa409ced3bc1fe2927a0a6e5f05416/cffi-2.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:799416bae98336e400981ff6e532d67d5c709cfb30afb79865a1315f94b0e224", size = 221808, upload-time = "2026-07-06T21:33:15.466Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/38/37/04f54b8e63a02f3d908332c9effbf8c366167c6f733ed8a3d4f79b7e2a1e/cffi-2.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:961be50688f7fba2fa65f63712d3b9b341a22311f5253460ce933f52f0de1c8c", size = 225241, upload-time = "2026-07-06T21:33:16.869Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a9/d6/c72eecca433cd3e681c65ed313ab4835d9d4a379704d0f628a6a05f51c2e/cffi-2.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bf5c6cf48238b0eb4c086978c492ad1cbc22373fc5b2d7353b3a598ce6db887a", size = 223588, upload-time = "2026-07-06T21:33:18.239Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c6/4b/e706f67279140f92939da3475ad610df18bfd52d50f14953a8e5fede71d5/cffi-2.1.0-cp313-cp313-win32.whl", hash = "sha256:db3eb7d46527159a878ec3460e9d40615bc25ba337d477db681aea6e4f05c5d2", size = 175248, upload-time = "2026-07-06T21:33:19.799Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/5a/47/59eb7975cb0e4ef0afa764ea945b29a5bb4537a9f771cb7d6c8a5dd74c95/cffi-2.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:8e74a6135550c4748af665b1b1118b6aab33b1fc6a16f9aff630af107c3b4512", size = 185717, upload-time = "2026-07-06T21:33:21.47Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/5a/af/34fee85c48f8d94efc8597bc09470c9dd274c145f1c12e0fbc6ab6d38d74/cffi-2.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:2282cd5e38aa8accd03e99d1256af8411c84cdbee6a89d841b563fdbd1f3e50f", size = 180114, upload-time = "2026-07-06T21:33:22.515Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d8/f0/81478e482afa03f6d18dc8f2afb5edc45b3080853b634b5ed91961be0998/cffi-2.1.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:d2117334c3af3bdcb9a88522b844a2bdb5efdc4f71c6c822df55486ae1c3347a", size = 194142, upload-time = "2026-07-06T21:33:23.657Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/7d/95/8de304305cd9204974b0ca051b86d307cafca13aa575a0ef1b44d92c0d8c/cffi-2.1.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:702c436735fbe99d59ada02a1f65cfc0d31c0ee8b7290912f8fbc5cd1e4b16c3", size = 196819, upload-time = "2026-07-06T21:33:25.007Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/20/71/7c8372d30e42415602ed9f268f7cfd66f1b855fed881ecd168bcb45dbc0b/cffi-2.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:1ff3456eab0d889592d1936d6125bbfbc7ae4d3354a700f8bd80450a66445d4d", size = 184965, upload-time = "2026-07-06T21:33:26.605Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d6/5c/584e626835f0375c928176c04137c96927165cb8733cdb3150ec04e5ee5e/cffi-2.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c4165821e131d6d4ca444347c2b694e2311bcfa3fe5a861cc72968f28867beac", size = 184952, upload-time = "2026-07-06T21:33:27.823Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/2e/d2/065fcae1c73979fac8e054462478d0ff8a29c40cdc2ed7ea5676a061df53/cffi-2.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:276f20fffd7b396e12516ba8edf9509210ac248cbbc5acbc39cd512f9f59ebe6", size = 222353, upload-time = "2026-07-06T21:33:29.178Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ed/a5/e8bbb1ce5b3ac2f53ad6a10bde44318a5a8d99d4f4a000d44a6e39aeb3e4/cffi-2.1.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:7d5980a3433d4b71a5e120f9dd551403d7824e31e2e67124fe2769c404c06913", size = 210051, upload-time = "2026-07-06T21:33:30.534Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/28/ed/c127d3ac36e899c965e3361357c3befacd6578c03f40125183e41c3b219e/cffi-2.1.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:6ca4919c6e4f89aa99c42510b42cf54596892c00b3f9077f6bdd1505e24b9c8d", size = 208630, upload-time = "2026-07-06T21:33:31.753Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/cc/d7/97d3136f81db489ec8d1d67748c110d6c994268fd7528014aa9f2b085e4e/cffi-2.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d53d10f7da99ae46f7373b9150393e9c5eab9b224909982b43832668de4779f5", size = 221593, upload-time = "2026-07-06T21:33:33.044Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d3/27/93195977168ee63aed233a1a0993a2178798654d1f4bddcdd321d6fd3b21/cffi-2.1.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c351efb95e832a853a29361675f33a7ce53de1a109cd73fd47af0712213aa4ce", size = 225146, upload-time = "2026-07-06T21:33:34.224Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b3/c1/6dbd291ee2ae5a50a034aa057207081f545923bbf15dad4511e985aafff5/cffi-2.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:dbf7c7a88e2bac086f06d14577332760bdeecc42bdec8ac4077f6260557d9326", size = 223240, upload-time = "2026-07-06T21:33:35.57Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/0f/6f/ade5ce9863a57992a6ea3d0d10d7e29b8749fc127204b3d493d667b2815f/cffi-2.1.0-cp314-cp314-win32.whl", hash = "sha256:1854b724d00f6654c742097d5387569021be12d3a0f770eae1df8f8acfcc6acd", size = 177723, upload-time = "2026-07-06T21:33:51.626Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/41/de/92b9eeed4ae4a21d6fd9b2a2c8505cbed573299902ea73981cc13f7ff62c/cffi-2.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:1b96bfe2c4bd825681b7d311ad6d9b7280a091f43e8f63da5729638083cd3bfb", size = 187937, upload-time = "2026-07-06T21:33:53.403Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/2e/1a/cc6ae6c2913a03aab8898eee57963cf1035b8df5872ed8b9115fcc7e2be8/cffi-2.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:7d28dff1db6764108bc30788d85d61c876beff416d9a49cb9dd7c5a9f34f5804", size = 183001, upload-time = "2026-07-06T21:33:54.74Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/14/f0/134c00ce0779ec86dea2aa1aac69339c2741a8045072676763512363a2ea/cffi-2.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7ea6b3e2c4250ff1de21c630fe72d0f63eb95c2c32ffbf64a358cf4a8836d714", size = 188538, upload-time = "2026-07-06T21:33:36.792Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/50/d8/3b86aba791cb610d24e8a3e1b2cd529e71fa15096b04e4d4e360049d4a4c/cffi-2.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6af371f3767faeffc6ac1ef57cdfd25844403e9d3f476c5537caee499de96376", size = 188230, upload-time = "2026-07-06T21:33:38.011Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/14/d0/117dcd9209255ad8571fbc8c92ef32593a1d294dcec91ddc4e4db50606f2/cffi-2.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eb4e8997a49aa2c08a3e43c9045d224448b8941d88e7ac163c7d383e560cbf98", size = 223899, upload-time = "2026-07-06T21:33:39.514Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b6/3d/f20f8b886b254e3ad10e15cd4186d3aed49f3e6a35ab37aab9f8f25f7c03/cffi-2.1.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:bf01d8c84cbea96b944c73b22182e6c7c432b3475632b8111dbfdc95ddad6e13", size = 211652, upload-time = "2026-07-06T21:33:40.851Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/28/3b/fad54de07260b93ddeef4b96d0131d57ea900675df1d410ae1deee52d7a6/cffi-2.1.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:33eb1ad83ebe8f313e0df035c406227d55a79456704a863fad9842136af5ad7d", size = 210755, upload-time = "2026-07-06T21:33:42.183Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/cc/82/3d5c705acb7abbba9bbd7d79b8e62e0f25b6120eb7ae6ac49f1b721722fe/cffi-2.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ac0f1a2d0cfa7eea3f2aaf006ab6e70e8feeb16b75d65b7e5939982ca2f11056", size = 223933, upload-time = "2026-07-06T21:33:43.603Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/6c/d0/47e338384ab6b1004241002fa616301020cea4fc95f283506565d252f276/cffi-2.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c16914df9fb7f500e440e6875fa23ff5e0b31db01fa9c06af98d59a91f0dc2e4", size = 226749, upload-time = "2026-07-06T21:33:45.046Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/70/25/65bd5b58ea4bfdfc15cde02cb5365f89ef8ab8b2adfb8fe5c4bd4233382f/cffi-2.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5ecbd0499275d57506d397eebe1981cee87b47fcd9ef5c22cab7ed7644a39a94", size = 225703, upload-time = "2026-07-06T21:33:46.374Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/dc/78/aa01ac599a8a4322533d45a1f9bc93b338276d2d59dabbe7c6d92a775c81/cffi-2.1.0-cp314-cp314t-win32.whl", hash = "sha256:7d034dcffa09e9a46c93fa3a3be402096cb5354ac6e41ab8e5cc9cd8b642ad76", size = 182857, upload-time = "2026-07-06T21:33:47.696Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b9/26/d00496b22de4d4228f32dde94ad996f350c8aad676d63bcca0743c8dea4d/cffi-2.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0582a58f3051372229ca8e7f5f589f9e5632678208d8636fea3676711fdf7fe5", size = 194065, upload-time = "2026-07-06T21:33:48.953Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d5/dd/0c7dbf815a579ff005008a2d815a55d6bb047c349eef536d9dc53d3f0a8d/cffi-2.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:510aeeeac94811b138077451da1fb18b308a5feab47dd2b603af55804155e1c8", size = 186404, upload-time = "2026-07-06T21:33:50.309Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/55/c7/8c8c50cb11c6750051daf12164098a9a6f027ac4356967fd4d800a07f242/cffi-2.1.0-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:2e9dabb9abcb7ad15938c7196ad5c1718a4e6d33cc79b4c0209bdb64c4a54a5c", size = 194121, upload-time = "2026-07-06T21:33:56.109Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/99/e2/67680bf19a6b60d2bb7ff83baefa2a4c3d2d7dc0f3277034b802e1fc504c/cffi-2.1.0-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:37f525a7e7e50c017fdebe58b787be310ad59357ae43a053943a6e1a6c526001", size = 196820, upload-time = "2026-07-06T21:33:57.288Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ed/da/4bbe583a3b3a5c8c60892124fe17f3fa3656523faf0d3484eae90f091853/cffi-2.1.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:95f2954c2c9473d892eca6e0409f3568b37ab62a8eedb122461f73cc273476e3", size = 184936, upload-time = "2026-07-06T21:33:58.765Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e5/4b/1f4c36ab273980d7aa75bb126ea4f8971f24a96108acad3a0a084028c57b/cffi-2.1.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:cdf2448aab5f661c9315308ec8b93f4e8a1a67a3c733f8631067a2b67d5913dc", size = 185045, upload-time = "2026-07-06T21:34:00.085Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ef/c3/ad299dc38f3583f8d916b299f028af418a9ec98bc695fcbebeae7420691c/cffi-2.1.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:90bec57cf82089383bd06a605b3eb8daebf7e5a668520beaf6e327a83a947699", size = 222342, upload-time = "2026-07-06T21:34:01.814Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/eb/d8/df4543cc087245044ed02ef3ad8e0a26619d0075ac7a77a12dc81177851b/cffi-2.1.0-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6274dcb2d15cef48daa73ed1be5a40d501d74dccd0cd6db364776d12cb6ba022", size = 210073, upload-time = "2026-07-06T21:34:03.255Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/2c/0e/fac738d73728c6cea2a88a2883dca54892496cbba88a1dc1f2909cb8a6f5/cffi-2.1.0-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:2b71d409cccee78310ab5dec549aed052aaea483346e282c7b02362596e01bb0", size = 208551, upload-time = "2026-07-06T21:34:04.433Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e6/3f/0b04a700dd64f465c93020253a793a82c9b4dff9961f48facd0df945d9b8/cffi-2.1.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7d3538f9c0e50670f4deb93dbb696576e60590369cae2faf7de681e597a8a1f1", size = 221649, upload-time = "2026-07-06T21:34:06.157Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/5d/7c/b7379a5704c79eda57ce075869ba70a0368d1c850f803b3c0d078d39dcaf/cffi-2.1.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:8f9ec95b8a043d3dfbc74d9abc6f7baf524dd27a8dc160b0a32ff9cdab650c28", size = 225203, upload-time = "2026-07-06T21:34:07.489Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/5a/02/d5e6c43ea85c41bda2a184a3418f195fe7cf602967a8d2b94e085b83deef/cffi-2.1.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:af5e2915d41fe6c961694d7bfdc8562942638200f3ce2765dfb8b745cf997629", size = 223263, upload-time = "2026-07-06T21:34:08.712Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/2c/d8/772b8259bf75749adffb1c546828978381fb516f60cf701f6c83daf60c85/cffi-2.1.0-cp315-cp315-win32.whl", hash = "sha256:0a42c688d19fca6e095a53c6a6e2295a5b050a8b289f109adab02a9e61a25de6", size = 177696, upload-time = "2026-07-06T21:34:26.355Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/2f/dd/afa2191fc6d57fedd26e5844a2fe2fcc0bbfa00961bbaa5a41e4921e7cca/cffi-2.1.0-cp315-cp315-win_amd64.whl", hash = "sha256:bccbbb5ee76a61f9d99b5bf3846a51d7fca4b6a732fe46f89295610edaf41853", size = 187914, upload-time = "2026-07-06T21:34:27.58Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/05/ef/6cd4f8c671517162379dc79cfae5aea9106bc38abb89628d5c16adf6a838/cffi-2.1.0-cp315-cp315-win_arm64.whl", hash = "sha256:8d35c139744adb3e727cd51b1a18324bbe44b8bd41bf8322bca4d41289f48eda", size = 183004, upload-time = "2026-07-06T21:34:28.905Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/11/b6/12fc55092817a5faa26fb8c40c7f9d662e11a46ee248c137aafc42517d92/cffi-2.1.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:f9912624a0c0b834b7520d7769b3644453aabc0a7e1c839da7359f050750e9bc", size = 188378, upload-time = "2026-07-06T21:34:09.926Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/8d/2e/cdac88979f295fde5daa69622c7d2111e56e7ceb94f211357fbe452339e4/cffi-2.1.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:df92f2aba50eb4d96718b68ef76f2e57a57b54f2fa62333496d16c6d585a85ca", size = 188319, upload-time = "2026-07-06T21:34:11.101Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e0/27/1d0b408497e41a74795af122d7b603c418c5fed0171450f899afd04e594f/cffi-2.1.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0520e1f4c35f44e209cbbb421b67eec42e6a157f59444dfb6058874ff3610e5d", size = 223904, upload-time = "2026-07-06T21:34:12.606Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/8b/31/e115c985105dd7ffb32444505f18ceb874bb42d992af05d5dced7ecf1980/cffi-2.1.0-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:3681e031db29958a7502f5c0c9d6bbc4c36cb20f7b104086fa642d1799631ff8", size = 211554, upload-time = "2026-07-06T21:34:13.987Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/5a/67/9e6e09409336d9e515c58367e7cfcf4f89df06ad25252675595a58eb59d5/cffi-2.1.0-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:762f99479dcb369f60ab9017ad4ab97a36a1dd7c1ee5a3b15db0f4b8659120cd", size = 210795, upload-time = "2026-07-06T21:34:15.972Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/19/e5/d3cc82a4a0be7902af279c04181ad038449c096734464a5ae1de3e1401bd/cffi-2.1.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0611e7ebf90573a535ebdc33ae9da222d037853983e13359f580fab781ca017f", size = 223843, upload-time = "2026-07-06T21:34:17.509Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b9/65/b434abc97ce7cecc2c640fde160507c0ecc7e21544b483ba3325d2e2ea17/cffi-2.1.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:86cf8755a791f72c85dc287128cc62d4f24d392e3f1e15837245623f4a33cccc", size = 226773, upload-time = "2026-07-06T21:34:19.05Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b5/9f/d4dc66ca651eb1145a133314cda721abf13cfac3d28c4a0402263ae6ad75/cffi-2.1.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:ba00f661f8ba35d075c937174e27c2c421cec3942fd2e0ea3e66996757c0fdd9", size = 225719, upload-time = "2026-07-06T21:34:20.576Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/68/5a/e536c528bc8057496c360c0978559a2dc45653f89dd6151078aa7d8fca1a/cffi-2.1.0-cp315-cp315t-win32.whl", hash = "sha256:cb96698e3c7413d906ce83f8ffd245ec1bd94707541f299d0ce4d6b0193e982b", size = 182760, upload-time = "2026-07-06T21:34:22.059Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d3/0b/0ffe8b82d3875bced5fa1e7986a7a46b748262a40ab7f60b475eb9fb1bb3/cffi-2.1.0-cp315-cp315t-win_amd64.whl", hash = "sha256:f146d154428a2523f9cc7936c02353c2459b8f6cf07d3cd1ee1c0a611109c5d5", size = 193769, upload-time = "2026-07-06T21:34:23.589Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a0/17/1073b53b68c9b5ca6914adf5f8bf55aacc2d3be102418c90700160ea8605/cffi-2.1.0-cp315-cp315t-win_arm64.whl", hash = "sha256:cbb7640ce37159548d2147b5b8c241f962143d4c71231431820783f4dc78f210", size = 186405, upload-time = "2026-07-06T21:34:24.857Z" }, -] - -[[package]] -name = "charset-normalizer" -version = "3.4.9" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" } -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/bd/2a/23f34ec9d04624958e137efdc394888716353190e75f25dd22c7a2c7a8aa/charset_normalizer-3.4.9.tar.gz", hash = "sha256:673611bbd43f0810bec0b0f028ddeaaa501190339cac411f347ac76917c3ae7b", size = 152439, upload-time = "2026-07-07T14:34:58.454Z" } -wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/70/4a/ecbd131485c07fcdfad54e28946d513e3da22ef3b4bd854dcafae54ec739/charset_normalizer-3.4.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:45b0cc4e3556cd875e09102988d1ab8356c998b596c9fced84547c8138b487a0", size = 319300, upload-time = "2026-07-07T14:33:15.666Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ec/96/5d9364e3342d69f3a045e1777bc47c85c383e6e9466d561b33fdb419d1f9/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9b2aff1c7b3884512b9512c3eaadd9bab39fb45042ffaaa1dd08ff2b9f8109d9", size = 215802, upload-time = "2026-07-07T14:33:17.031Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/4b/4c/5361f9aa7f2cb58d94f2ab831b3d493f69efb1d239654b4744e3c09527cb/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9104ed0bd76a429d46f9ec0dbc9b08ad1d2dcdf2b00a5a0daa1c145329b35b44", size = 237171, upload-time = "2026-07-07T14:33:18.576Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/50/78/ce342ca4ff30b2eb49fe6d9578df85974f90c67d294113e94efdd9664cbd/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7b86a2b16095d250c6f58b3d9b2eee6f4147754344f3dab0922f7c9bf7d226c9", size = 233075, upload-time = "2026-07-07T14:33:20.084Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/01/c4/4fa4c8b3097a11f3c5f09a35b72ed6855fb1d332469504962ab7bafcc702/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e226f6218febc71f6c1fc2fafb91c226f75bdc1d8fb12d66823716e891608fd", size = 224256, upload-time = "2026-07-07T14:33:21.747Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/87/3a/ad914516df7e358a81aae018caa5e0470ba827fa6d763b1d2e87d920a5f6/charset_normalizer-3.4.9-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:90c44bc373b7687f6948b693cceaea1348ae0975d7474746559494468e3c1d84", size = 208784, upload-time = "2026-07-07T14:33:23.313Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d7/74/3c12f9755717dfe5c5c87da63f35d765fa0c00382ec26bf23f7fae34f2ba/charset_normalizer-3.4.9-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cdef90ae47919cae358d8ab15797a800ed41da7aba5d72419fb510729e2ed4b", size = 219928, upload-time = "2026-07-07T14:33:24.814Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/33/9a/895095b83e7907abd6d3d99aad3a38ad0d9686cc186cb0c94c24320fe63e/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:60f44ade2cf573dad7a277e6f8ca9a51a21dda572b13bd7d8539bb3cd5dbedde", size = 218489, upload-time = "2026-07-07T14:33:26.42Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a1/34/ef5c05f412f42520d7709b7d3784d19640839eb7366ded1755511585429f/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:a1786910334ed46ab1dd73222f2cd1e05c2c3bb39f6dddb4f8b36fc382058a39", size = 210267, upload-time = "2026-07-07T14:33:27.952Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/83/dc/9b29fa4412b318bf3bfea985c35d67eb55e04b59a7c3f2237168b0e0be6f/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:03d07803992c6c7bbc976327f34b18b6160327fc81cb82c9d504720ac0be3b62", size = 226030, upload-time = "2026-07-07T14:33:29.397Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/0e/42/6dbc00b8cd16011691203e33570fa42ed5746599a2e878112d16eab403a3/charset_normalizer-3.4.9-cp312-cp312-win32.whl", hash = "sha256:78841cccf1af7b40f6f716338d50c0902dbe88d9f800b3c973b7a9a0a693a642", size = 151185, upload-time = "2026-07-07T14:33:30.781Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/80/cc/f920afd1a23c58ccd53c1d36085a71893a4737ff5e66e0371efab6809850/charset_normalizer-3.4.9-cp312-cp312-win_amd64.whl", hash = "sha256:4b3dac63058cc36820b0dd072f89898604e2d39686fe05321729d00d8ac185a0", size = 162557, upload-time = "2026-07-07T14:33:32.176Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f0/e6/0386d43a261ff4e4b30c5857af7df877254b46bec7b9d1b74b6bf969a90b/charset_normalizer-3.4.9-cp312-cp312-win_arm64.whl", hash = "sha256:78fa18e436a1a0e58dbd7e02fc4473f3f32cceb12df9dfca542d075961c307d2", size = 152665, upload-time = "2026-07-07T14:33:33.711Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b2/06/97ec2aeae780b31d742b6352218b43841a6871e2564578ca522dce4a45c3/charset_normalizer-3.4.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:440eede837960000d74978f0eba527be106b5b9aee0daf779d395276ed0b0614", size = 317688, upload-time = "2026-07-07T14:33:35.408Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d0/39/8ff066c672434225f8d25f8b739f992af250944392173dcc88362681c9bf/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21e764fd1e70b6a3e205a0e46f3051701f98a8cb3fad66eeb80e48bb502f8698", size = 214982, upload-time = "2026-07-07T14:33:36.996Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/92/8f/3a47a3667c83c2df9483d91644c6c107de3bf8874aa1793da9d3012eb986/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e4fd89cc178bced6ad29cb3e6dd4aa63fa5017c3524dbd0b25998fb64a87cc8b", size = 236460, upload-time = "2026-07-07T14:33:38.536Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f1/60/b22cdbee7e4013dab8b0d7647fc6181120fbbbc8f7025c226d15bd5a47fc/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bd47ba7fc3ca94896759ea0109775132d3e7ab921fbf54038e1bab2e46c313c9", size = 232003, upload-time = "2026-07-07T14:33:40.059Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ea/f8/72eb13dcabe7257035cea8aefd922caad2f110d252bf9f67c4c2ca763aee/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:84fd18bcc17526fc2b3c1af7d2b9217d32c9c04448c16ec693b9b4f1985c3d33", size = 223149, upload-time = "2026-07-07T14:33:41.631Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b0/3e/faee8f9de92b14ee1198e9163252bb15efee7301b31256a3b6d9ebfdd0dd/charset_normalizer-3.4.9-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:5b10cd92fc5c498b35a8635df6d5a100207f88b63a4dc1de7ef9a548e1e2cd63", size = 207901, upload-time = "2026-07-07T14:33:43.209Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/3a/25/45f30093ae27dd7b92a793b61882a38685f993700113ca36e0c9c14965e1/charset_normalizer-3.4.9-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a4fbdde9dd4a9ce5fd52c2b3a347bb50cc89483ef783f1cb00d408c13f7a96c0", size = 219176, upload-time = "2026-07-07T14:33:44.725Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/48/18/c8f397329c35e32f6a837e488986f4ae03bd2abebc453b48714991630c2f/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:416c229f77e5ea25b3dfd4b582f8d73d7e43c22320302b9ab128a2d3a0b38efe", size = 217356, upload-time = "2026-07-07T14:33:46.192Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/86/7e/5ce0bba863470fd1902d5e5843968951bddf38abe4742fc97116ef4598b3/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:75286256590a6320cf106a0d28970d3560aad9ee09aa7b34fb40524792436d35", size = 209614, upload-time = "2026-07-07T14:33:47.705Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/6c/ef/2473d3c4d869155be4af1191111d59c4d5c4e0173026f7e85b176e23bf65/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:69b157c5d3292bcd443faca052f3096f637f1e074b98212a933c074ae23dc3b8", size = 224991, upload-time = "2026-07-07T14:33:49.238Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d0/a3/53ddae3db108a088156aa8ddfafd411ebbc1340f48c5573f697b27f69a39/charset_normalizer-3.4.9-cp313-cp313-win32.whl", hash = "sha256:51307f5c71007673a2bf8232ad973483d281e74cb99c8c5a990af1eefa6277d9", size = 150622, upload-time = "2026-07-07T14:33:50.711Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e8/ef/6953a77c7cf2c2ff9998e6f575ab3e380119f100223381565a4f94c1f836/charset_normalizer-3.4.9-cp313-cp313-win_amd64.whl", hash = "sha256:fe2c7201c642b7c308f1675355ad7ff7b66acfe3541625efe5a3ad38f29d6115", size = 161947, upload-time = "2026-07-07T14:33:52.197Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/6e/fb/d560d1d1555debbfe7849d9cac6145c1b537709d79576bf22557ed803b82/charset_normalizer-3.4.9-cp313-cp313-win_arm64.whl", hash = "sha256:611057cc5d5c0afc743ba8be6bd828c17e0aaa8643f9d0a9b9bb7dea80eb8012", size = 152594, upload-time = "2026-07-07T14:33:53.486Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/7e/8d/496817fa0944239ecae662dd57ea765cfeaec6a735f9f025d4b7b72e7143/charset_normalizer-3.4.9-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:0327fcd59a935777d83410750c50600ee9571af2846f71ce40f25b13da1ef380", size = 317253, upload-time = "2026-07-07T14:33:54.994Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/2b/f9/ef4a69ea338ad3c0deceea0f5f7d2380ae8b52132b06d652cb0d2cd86706/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a79d9f4d8001473a30c163556b3c3bfebec837495a412dde78b51672f6134f9", size = 215898, upload-time = "2026-07-07T14:33:56.334Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/8c/e7/5ddfd76fc061eb52de219658a4aa431cbacadf0a0219c8854f00da50d289/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:33bdcc2a32c0a0e861f60841a512c8acc658c87c2ac59d89e3a46dacf7d866e4", size = 236718, upload-time = "2026-07-07T14:33:57.9Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/49/ba/768fa3f36048d81c477a0ce61f813bc1454d80917ccfe550abd9f44f5e24/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f840ed6d8ecba8255df8c42b87fadeda98ddfc6eeec05e2dc66e26d46dd6f58a", size = 232519, upload-time = "2026-07-07T14:33:59.811Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f4/c4/b3e049d2aa3766180c78507110543d9d50894cc97f57de543f1be521dcdc/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c25fe15c70c59eb7c5ce8c06a1f3fa1da0ecc5ea1e7a5922c40fd2fa9b0d5046", size = 223143, upload-time = "2026-07-07T14:34:01.517Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/19/79/55c32d06d76ae4feafe053f061f3e3ab70bcf19f4007797ce8c3efda7830/charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:f7fb7d750cfa0a070d2c24e831fd3481019a60dd317ea2b39acbcebc08b6ed81", size = 206742, upload-time = "2026-07-07T14:34:03.04Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/10/e0/47c079dd82d217c807479cd59ffd30af56307ea31c108b75758970459ad3/charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4d1c96a7a18b9690a4d46df09e3e3382406ae3213727cd1019ebade1c4a81917", size = 219191, upload-time = "2026-07-07T14:34:04.657Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/42/ab/b9bc2e77d6b44a7e46ef62ec5cac1c9a6ba7b9135a5d560f002696ec9995/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a4cfde78a9f2880208d16a93b795726a3017d5977e08d1e162a7a31322479c41", size = 218328, upload-time = "2026-07-07T14:34:06.115Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f1/78/c9c71d599f5aa2d42bcdd35cbbd46d7f535351a57e40ff7d8e5a7e219401/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d4d6fcde76f94f5cb9e43e9e9a61f16dacefd228cbbf6f1a09bd9b219a92f1a1", size = 207406, upload-time = "2026-07-07T14:34:07.554Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f6/39/c914445c321a845097ce4f6ac7de9a18228a77b766272125a1ce00d851eb/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:898f0e9068ca27d37f8e83a5b962821df851532e6c4a7d615c1c033f9da6eedf", size = 225157, upload-time = "2026-07-07T14:34:09.061Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/9b/f2/c0d4b8508565a36bc5c624e88ed297f5b0b1095011034d7f5b83a69908b5/charset_normalizer-3.4.9-cp314-cp314-win32.whl", hash = "sha256:c1c948747b03be832dceed96ca815cef7360de9aa19d37c730f8e3f6101aca48", size = 151095, upload-time = "2026-07-07T14:34:10.901Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/49/fd/a1d26144398c67486422a72bf5812cda22cb4ccfcd95a290fb41ceb4b8e2/charset_normalizer-3.4.9-cp314-cp314-win_amd64.whl", hash = "sha256:16b65ea0f2465b6fb52aa22de5eca612aa964ddfec00a912e26f4656cbef890b", size = 162796, upload-time = "2026-07-07T14:34:12.47Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/20/95/d75e82f8ce9fd323ebf059c16c9aadefb22a1ecde13b7840b35835e4886c/charset_normalizer-3.4.9-cp314-cp314-win_arm64.whl", hash = "sha256:40a126142a56b2dfc0aacbad1de8310cbf60da7656db0e6b16eebd48e3e93519", size = 153334, upload-time = "2026-07-07T14:34:14.044Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/00/5e/17398df3a139985ba9d11ed072531986f408c8fca952835ef1ab1820c02b/charset_normalizer-3.4.9-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:609b3ba8fcc0fb5ab7af00719d0fb6ad0cb518e48e7712d12fd68f1327951198", size = 338848, upload-time = "2026-07-07T14:34:15.688Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/cd/91/7253a32e86b7e1d1239b1b36ba6dd0f021a21107ab33054b53119cc083b9/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51447e9aa2684679af07ca5021c3db526e0284347ebf4ffcec1154c3350cfe32", size = 223022, upload-time = "2026-07-07T14:34:17.248Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/cb/32/2e64bd2be10e89c61e57ebe6a93fd98ae88eb7ebe414b5121f22c96c69eb/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cc1b0fff8ead343dae06305f954eb8468ba0ec1a97881f42489d198e4ce3c632", size = 241590, upload-time = "2026-07-07T14:34:18.813Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/3d/ef/d96ec496cfea0c21db43b0ad03891308b02388d054cc902cf0e5a1ad6a88/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fa36ec09ef71d158186bc79e359ff5fdd6e7996fe8ab638f00d6b93139ba4fcf", size = 239584, upload-time = "2026-07-07T14:34:20.52Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d4/ce/9af95f7876194bd7a14e3dfe4a4de2e0bff02666a3910d72beafd06cc297/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:df115d4d83168fdf2cae48ef1ff6d1cb4c466364e30861b37121de0f3bf1b990", size = 230224, upload-time = "2026-07-07T14:34:22.189Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/52/94/af74dde74a3996bd959c350709bfe50e297823d70a8c1cbd54b838880863/charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f86c6358749bd4fda175388691e3ba8c46e24c5347d0afd20f9b7edfc9faf07d", size = 212667, upload-time = "2026-07-07T14:34:23.857Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ee/f0/f1c4fe746c395922961b5916ed1d7d6e7d4c84851d19ed43cc89980ec953/charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:32286a2c8d167e897177b673176c1e3e00d4057caf5d2b64eef9a3666b03018e", size = 227179, upload-time = "2026-07-07T14:34:25.586Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e4/56/6c745619ac397e8871e2bcd3cea1eec86b877488f33888b3aef5c3ed506e/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:83aed2c10721ddd90f68140685391b50811a880af20654c59af6b6c66c40513c", size = 225372, upload-time = "2026-07-07T14:34:27.212Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/78/ad/98aae8630ac71f16711968e38a5acfecce41b778bf2f0312851020f565a8/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cd6c3d4b783c556fa00bf540854e42f135e2f256abd29669fcd0da0f2dec79c2", size = 215222, upload-time = "2026-07-07T14:34:28.774Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f7/40/9593d54209765207a7f11073c06494c1721e4ca4a0a426c597679bf7f91e/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ee2f2a527e3c1a6e6411eb4209642e138b544a2d72fe5d0d76daf77b24063534", size = 231958, upload-time = "2026-07-07T14:34:30.345Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b1/27/693ee5e8a18191eb38647360c51cd505013e2bd3b366aa43fd5344c21e3c/charset_normalizer-3.4.9-cp314-cp314t-win32.whl", hash = "sha256:0d861473f743244d349b50f850d10eb87aeb22bbdcc8e64f79273c94af5a8226", size = 155580, upload-time = "2026-07-07T14:34:31.884Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/80/3f/bd97d3d9c613013d07cb7733d299385b41df37f0471310f5a73dc359f0b8/charset_normalizer-3.4.9-cp314-cp314t-win_amd64.whl", hash = "sha256:9b8e0f3107e2200b76f6054de99016eac3ee6762713587b36baaa7e4bd2ae177", size = 167620, upload-time = "2026-07-07T14:34:33.438Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/3d/c6/eee9dca4439b1061f76373f06ea855678cc4a64c1c3c90b50e479edbb8eb/charset_normalizer-3.4.9-cp314-cp314t-win_arm64.whl", hash = "sha256:19ac87f93086ce37b86e098888555c4b4bc48102279bae3350098c0ed664b501", size = 158037, upload-time = "2026-07-07T14:34:35.018Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/98/2b/f97f1c193fb855c345d678f5077d6926034db0722df74c8f057020e05a25/charset_normalizer-3.4.9-py3-none-any.whl", hash = "sha256:68e5f26a1ad57ded6d1cfb85331d1c1a195314756471d97758c48498bb4dcdf5", size = 64538, upload-time = "2026-07-07T14:34:56.993Z" }, + { url = "https://files.pythonhosted.org/packages/1e/85/990925db5df586ec90beb97529c853497e7f85ba0234830447faf41c3057/cffi-2.1.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:df2b82571a1b30f58a87bf4e5a9e78d2b1eff6c6ce8fd3aa3757221f93f0863f", size = 184829, upload-time = "2026-07-06T21:32:44.324Z" }, + { url = "https://files.pythonhosted.org/packages/4b/92/e7bb136ad6b5352603732cf907ef862ca103f20f2031c1735a46300c20c9/cffi-2.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:78474632761faa0fb96f30b1c928c84ebcf68713cbb80d15bab09dfe61640fde", size = 184728, upload-time = "2026-07-06T21:32:45.683Z" }, + { url = "https://files.pythonhosted.org/packages/c3/c0/d1ec30ffb370f748f2fb54425972bfef9871e0132e82fb589c46b6676049/cffi-2.1.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:5972433ad71a9e46516584ef60a0fda12d9dc459938d1539c3ddecf9bdc1368d", size = 214815, upload-time = "2026-07-06T21:32:48.557Z" }, + { url = "https://files.pythonhosted.org/packages/1b/dc/5620cf930688be01f2d673804291de757a934c90b946dbdc3d84130c2ea4/cffi-2.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b6422532152adf4e59b110cb2808cee7a033800952f5c036b4af047ee43199e7", size = 222429, upload-time = "2026-07-06T21:32:49.848Z" }, + { url = "https://files.pythonhosted.org/packages/4b/a4/77b53abbf7a1e0beb9637edbef2a94d15f9c822f591e85d439ffd91519a6/cffi-2.1.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:46b1c8db8f6122420f32d02fffb924c2fe9bc772d228c7c711748fff56aabb2b", size = 210315, upload-time = "2026-07-06T21:32:51.221Z" }, + { url = "https://files.pythonhosted.org/packages/58/0c/f528df19cc94b675087324d4760d9e6d5bfae97d6217aa4fac43de4f5fcc/cffi-2.1.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9fafc5aa2e2a39aaf7f8cc0c1f044a9b07fca12e558dca53a3cc5c654ad67a7", size = 208859, upload-time = "2026-07-06T21:32:52.512Z" }, + { url = "https://files.pythonhosted.org/packages/62/f2/c9522a81c32132799a1972c39f5c5f8b4c8b9f00488a23feaa6c06f07741/cffi-2.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1e9f50d192a3e525b15a75ab5114e442d83d657b7ec29182a991bc9a88fd3a66", size = 221844, upload-time = "2026-07-06T21:32:53.704Z" }, + { url = "https://files.pythonhosted.org/packages/6e/28/bd53988b9833e8f8ad539d26f4c07a6b3f6bcb1e9e02e7ca038250b3428d/cffi-2.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:98fff996e983a36d3aa2eca83af40c5821202e7e6f32d13ae94e3d2286f10cfe", size = 225287, upload-time = "2026-07-06T21:32:54.907Z" }, + { url = "https://files.pythonhosted.org/packages/79/99/0d0fd37f055224085f42bbb2c022d002e17dde4a97972822327b07d84101/cffi-2.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:379de10ce1ba048b1448599d1b37b24caee16309d1ac98d3982fc997f768700b", size = 223681, upload-time = "2026-07-06T21:32:56.329Z" }, + { url = "https://files.pythonhosted.org/packages/b0/80/c138990aa2a70b1a269f6e06348729836d733d6f970867943f61d367f8cc/cffi-2.1.0-cp312-cp312-win32.whl", hash = "sha256:9b8f0f26ca4e7513c534d351eca551947d053fac438f2a04ac96d882909b0d3a", size = 175269, upload-time = "2026-07-06T21:32:57.777Z" }, + { url = "https://files.pythonhosted.org/packages/a8/eb/f636456ff21a83fc13c032b58cc5dde061691546ac79efa284b2989b7982/cffi-2.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:c97f080ea627e2863524c5af3836e2270b5f5dfff1f104392b959f8df0c5d384", size = 185881, upload-time = "2026-07-06T21:32:59.253Z" }, + { url = "https://files.pythonhosted.org/packages/dd/2c/400ea43e721727dca8a65c4521390e9196757caba4a45643acb2b63271b8/cffi-2.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:6d194185eabd279f1c05ebe3504265ddfc5ad2b58d0714f7db9f01da592e9eb6", size = 180088, upload-time = "2026-07-06T21:33:02.278Z" }, + { url = "https://files.pythonhosted.org/packages/96/88/a996879e2eeccb815f6e3a5967b12a308257412acec882039d386bd2aa7b/cffi-2.1.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:10537b1df4967ca26d21e5072d7d54188354483b91dc75058968d3f0cf13fbda", size = 194331, upload-time = "2026-07-06T21:33:03.697Z" }, + { url = "https://files.pythonhosted.org/packages/58/85/7ae00d5c8dd6266f4e944c3db630f3c5c9a98b61d469c714d848b1d8138a/cffi-2.1.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:a95b05f9baf29b91171b3a8bd2020b028835243e7b0ff6bb23e2a3c228518b1b", size = 196966, upload-time = "2026-07-06T21:33:05.353Z" }, + { url = "https://files.pythonhosted.org/packages/8c/e9/45c3a76ad8d43ad9261f4c95436da61128d3ca545d72b9612c0ab5be0b1c/cffi-2.1.0-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:15faec4adfff450819f3aee0e2e02c812de6edb88203aa58807955db2003472a", size = 184795, upload-time = "2026-07-06T21:33:06.699Z" }, + { url = "https://files.pythonhosted.org/packages/84/4c/82f132cb4418ee6d953d982b19191e87e2a6372c8a4ce36e50b69d6ade4a/cffi-2.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:716ff8ec22f20b4d988b12884086bcef0fc99737043e503f7a3935a6be99b1ea", size = 184746, upload-time = "2026-07-06T21:33:08.071Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1c/4ed5a0e5bdca6cbc275556de3328dd1b76fd0c11cc13c88fe66d1d8715f2/cffi-2.1.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:63960549e4f8dc41e31accb97b975abaecfc44c03e396c093a6436763c2ea7db", size = 214747, upload-time = "2026-07-06T21:33:09.671Z" }, + { url = "https://files.pythonhosted.org/packages/3a/a6/e879bb68cc23a2bc9ba8f4b7d8019f0c2694bad2ab6c4a3701d429439f58/cffi-2.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ff067a8d8d880e7809e4ac88eb009bb848870115317b306666502ccad30b147f", size = 222392, upload-time = "2026-07-06T21:33:10.896Z" }, + { url = "https://files.pythonhosted.org/packages/88/f6/01890cfd63c08f8eb96a8319b0443690197d240a8bd6346048cf7bde9190/cffi-2.1.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:3b926723c13eba9f81d2ef3820d63aeceec3b2d4639906047bf675cb8a7a500d", size = 210285, upload-time = "2026-07-06T21:33:12.251Z" }, + { url = "https://files.pythonhosted.org/packages/a6/cf/2b684132056f438567b61e19d690dd31cd0921ace051e0a458be6074369e/cffi-2.1.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:47ff3a8bfd8cb9da1af7524b965127095055654c177fcfc7578debcb015eecd0", size = 208801, upload-time = "2026-07-06T21:33:13.617Z" }, + { url = "https://files.pythonhosted.org/packages/6f/08/f2e7d62c460faae0926f2d6e423694aa409ced3bc1fe2927a0a6e5f05416/cffi-2.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:799416bae98336e400981ff6e532d67d5c709cfb30afb79865a1315f94b0e224", size = 221808, upload-time = "2026-07-06T21:33:15.466Z" }, + { url = "https://files.pythonhosted.org/packages/38/37/04f54b8e63a02f3d908332c9effbf8c366167c6f733ed8a3d4f79b7e2a1e/cffi-2.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:961be50688f7fba2fa65f63712d3b9b341a22311f5253460ce933f52f0de1c8c", size = 225241, upload-time = "2026-07-06T21:33:16.869Z" }, + { url = "https://files.pythonhosted.org/packages/a9/d6/c72eecca433cd3e681c65ed313ab4835d9d4a379704d0f628a6a05f51c2e/cffi-2.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bf5c6cf48238b0eb4c086978c492ad1cbc22373fc5b2d7353b3a598ce6db887a", size = 223588, upload-time = "2026-07-06T21:33:18.239Z" }, + { url = "https://files.pythonhosted.org/packages/c6/4b/e706f67279140f92939da3475ad610df18bfd52d50f14953a8e5fede71d5/cffi-2.1.0-cp313-cp313-win32.whl", hash = "sha256:db3eb7d46527159a878ec3460e9d40615bc25ba337d477db681aea6e4f05c5d2", size = 175248, upload-time = "2026-07-06T21:33:19.799Z" }, + { url = "https://files.pythonhosted.org/packages/5a/47/59eb7975cb0e4ef0afa764ea945b29a5bb4537a9f771cb7d6c8a5dd74c95/cffi-2.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:8e74a6135550c4748af665b1b1118b6aab33b1fc6a16f9aff630af107c3b4512", size = 185717, upload-time = "2026-07-06T21:33:21.47Z" }, + { url = "https://files.pythonhosted.org/packages/5a/af/34fee85c48f8d94efc8597bc09470c9dd274c145f1c12e0fbc6ab6d38d74/cffi-2.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:2282cd5e38aa8accd03e99d1256af8411c84cdbee6a89d841b563fdbd1f3e50f", size = 180114, upload-time = "2026-07-06T21:33:22.515Z" }, + { url = "https://files.pythonhosted.org/packages/d8/f0/81478e482afa03f6d18dc8f2afb5edc45b3080853b634b5ed91961be0998/cffi-2.1.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:d2117334c3af3bdcb9a88522b844a2bdb5efdc4f71c6c822df55486ae1c3347a", size = 194142, upload-time = "2026-07-06T21:33:23.657Z" }, + { url = "https://files.pythonhosted.org/packages/7d/95/8de304305cd9204974b0ca051b86d307cafca13aa575a0ef1b44d92c0d8c/cffi-2.1.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:702c436735fbe99d59ada02a1f65cfc0d31c0ee8b7290912f8fbc5cd1e4b16c3", size = 196819, upload-time = "2026-07-06T21:33:25.007Z" }, + { url = "https://files.pythonhosted.org/packages/20/71/7c8372d30e42415602ed9f268f7cfd66f1b855fed881ecd168bcb45dbc0b/cffi-2.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:1ff3456eab0d889592d1936d6125bbfbc7ae4d3354a700f8bd80450a66445d4d", size = 184965, upload-time = "2026-07-06T21:33:26.605Z" }, + { url = "https://files.pythonhosted.org/packages/d6/5c/584e626835f0375c928176c04137c96927165cb8733cdb3150ec04e5ee5e/cffi-2.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c4165821e131d6d4ca444347c2b694e2311bcfa3fe5a861cc72968f28867beac", size = 184952, upload-time = "2026-07-06T21:33:27.823Z" }, + { url = "https://files.pythonhosted.org/packages/2e/d2/065fcae1c73979fac8e054462478d0ff8a29c40cdc2ed7ea5676a061df53/cffi-2.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:276f20fffd7b396e12516ba8edf9509210ac248cbbc5acbc39cd512f9f59ebe6", size = 222353, upload-time = "2026-07-06T21:33:29.178Z" }, + { url = "https://files.pythonhosted.org/packages/ed/a5/e8bbb1ce5b3ac2f53ad6a10bde44318a5a8d99d4f4a000d44a6e39aeb3e4/cffi-2.1.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:7d5980a3433d4b71a5e120f9dd551403d7824e31e2e67124fe2769c404c06913", size = 210051, upload-time = "2026-07-06T21:33:30.534Z" }, + { url = "https://files.pythonhosted.org/packages/28/ed/c127d3ac36e899c965e3361357c3befacd6578c03f40125183e41c3b219e/cffi-2.1.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:6ca4919c6e4f89aa99c42510b42cf54596892c00b3f9077f6bdd1505e24b9c8d", size = 208630, upload-time = "2026-07-06T21:33:31.753Z" }, + { url = "https://files.pythonhosted.org/packages/cc/d7/97d3136f81db489ec8d1d67748c110d6c994268fd7528014aa9f2b085e4e/cffi-2.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d53d10f7da99ae46f7373b9150393e9c5eab9b224909982b43832668de4779f5", size = 221593, upload-time = "2026-07-06T21:33:33.044Z" }, + { url = "https://files.pythonhosted.org/packages/d3/27/93195977168ee63aed233a1a0993a2178798654d1f4bddcdd321d6fd3b21/cffi-2.1.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c351efb95e832a853a29361675f33a7ce53de1a109cd73fd47af0712213aa4ce", size = 225146, upload-time = "2026-07-06T21:33:34.224Z" }, + { url = "https://files.pythonhosted.org/packages/b3/c1/6dbd291ee2ae5a50a034aa057207081f545923bbf15dad4511e985aafff5/cffi-2.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:dbf7c7a88e2bac086f06d14577332760bdeecc42bdec8ac4077f6260557d9326", size = 223240, upload-time = "2026-07-06T21:33:35.57Z" }, + { url = "https://files.pythonhosted.org/packages/0f/6f/ade5ce9863a57992a6ea3d0d10d7e29b8749fc127204b3d493d667b2815f/cffi-2.1.0-cp314-cp314-win32.whl", hash = "sha256:1854b724d00f6654c742097d5387569021be12d3a0f770eae1df8f8acfcc6acd", size = 177723, upload-time = "2026-07-06T21:33:51.626Z" }, + { url = "https://files.pythonhosted.org/packages/41/de/92b9eeed4ae4a21d6fd9b2a2c8505cbed573299902ea73981cc13f7ff62c/cffi-2.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:1b96bfe2c4bd825681b7d311ad6d9b7280a091f43e8f63da5729638083cd3bfb", size = 187937, upload-time = "2026-07-06T21:33:53.403Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1a/cc6ae6c2913a03aab8898eee57963cf1035b8df5872ed8b9115fcc7e2be8/cffi-2.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:7d28dff1db6764108bc30788d85d61c876beff416d9a49cb9dd7c5a9f34f5804", size = 183001, upload-time = "2026-07-06T21:33:54.74Z" }, + { url = "https://files.pythonhosted.org/packages/14/f0/134c00ce0779ec86dea2aa1aac69339c2741a8045072676763512363a2ea/cffi-2.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7ea6b3e2c4250ff1de21c630fe72d0f63eb95c2c32ffbf64a358cf4a8836d714", size = 188538, upload-time = "2026-07-06T21:33:36.792Z" }, + { url = "https://files.pythonhosted.org/packages/50/d8/3b86aba791cb610d24e8a3e1b2cd529e71fa15096b04e4d4e360049d4a4c/cffi-2.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6af371f3767faeffc6ac1ef57cdfd25844403e9d3f476c5537caee499de96376", size = 188230, upload-time = "2026-07-06T21:33:38.011Z" }, + { url = "https://files.pythonhosted.org/packages/14/d0/117dcd9209255ad8571fbc8c92ef32593a1d294dcec91ddc4e4db50606f2/cffi-2.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eb4e8997a49aa2c08a3e43c9045d224448b8941d88e7ac163c7d383e560cbf98", size = 223899, upload-time = "2026-07-06T21:33:39.514Z" }, + { url = "https://files.pythonhosted.org/packages/b6/3d/f20f8b886b254e3ad10e15cd4186d3aed49f3e6a35ab37aab9f8f25f7c03/cffi-2.1.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:bf01d8c84cbea96b944c73b22182e6c7c432b3475632b8111dbfdc95ddad6e13", size = 211652, upload-time = "2026-07-06T21:33:40.851Z" }, + { url = "https://files.pythonhosted.org/packages/28/3b/fad54de07260b93ddeef4b96d0131d57ea900675df1d410ae1deee52d7a6/cffi-2.1.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:33eb1ad83ebe8f313e0df035c406227d55a79456704a863fad9842136af5ad7d", size = 210755, upload-time = "2026-07-06T21:33:42.183Z" }, + { url = "https://files.pythonhosted.org/packages/cc/82/3d5c705acb7abbba9bbd7d79b8e62e0f25b6120eb7ae6ac49f1b721722fe/cffi-2.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ac0f1a2d0cfa7eea3f2aaf006ab6e70e8feeb16b75d65b7e5939982ca2f11056", size = 223933, upload-time = "2026-07-06T21:33:43.603Z" }, + { url = "https://files.pythonhosted.org/packages/6c/d0/47e338384ab6b1004241002fa616301020cea4fc95f283506565d252f276/cffi-2.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c16914df9fb7f500e440e6875fa23ff5e0b31db01fa9c06af98d59a91f0dc2e4", size = 226749, upload-time = "2026-07-06T21:33:45.046Z" }, + { url = "https://files.pythonhosted.org/packages/70/25/65bd5b58ea4bfdfc15cde02cb5365f89ef8ab8b2adfb8fe5c4bd4233382f/cffi-2.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5ecbd0499275d57506d397eebe1981cee87b47fcd9ef5c22cab7ed7644a39a94", size = 225703, upload-time = "2026-07-06T21:33:46.374Z" }, + { url = "https://files.pythonhosted.org/packages/dc/78/aa01ac599a8a4322533d45a1f9bc93b338276d2d59dabbe7c6d92a775c81/cffi-2.1.0-cp314-cp314t-win32.whl", hash = "sha256:7d034dcffa09e9a46c93fa3a3be402096cb5354ac6e41ab8e5cc9cd8b642ad76", size = 182857, upload-time = "2026-07-06T21:33:47.696Z" }, + { url = "https://files.pythonhosted.org/packages/b9/26/d00496b22de4d4228f32dde94ad996f350c8aad676d63bcca0743c8dea4d/cffi-2.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0582a58f3051372229ca8e7f5f589f9e5632678208d8636fea3676711fdf7fe5", size = 194065, upload-time = "2026-07-06T21:33:48.953Z" }, + { url = "https://files.pythonhosted.org/packages/d5/dd/0c7dbf815a579ff005008a2d815a55d6bb047c349eef536d9dc53d3f0a8d/cffi-2.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:510aeeeac94811b138077451da1fb18b308a5feab47dd2b603af55804155e1c8", size = 186404, upload-time = "2026-07-06T21:33:50.309Z" }, + { url = "https://files.pythonhosted.org/packages/55/c7/8c8c50cb11c6750051daf12164098a9a6f027ac4356967fd4d800a07f242/cffi-2.1.0-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:2e9dabb9abcb7ad15938c7196ad5c1718a4e6d33cc79b4c0209bdb64c4a54a5c", size = 194121, upload-time = "2026-07-06T21:33:56.109Z" }, + { url = "https://files.pythonhosted.org/packages/99/e2/67680bf19a6b60d2bb7ff83baefa2a4c3d2d7dc0f3277034b802e1fc504c/cffi-2.1.0-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:37f525a7e7e50c017fdebe58b787be310ad59357ae43a053943a6e1a6c526001", size = 196820, upload-time = "2026-07-06T21:33:57.288Z" }, + { url = "https://files.pythonhosted.org/packages/ed/da/4bbe583a3b3a5c8c60892124fe17f3fa3656523faf0d3484eae90f091853/cffi-2.1.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:95f2954c2c9473d892eca6e0409f3568b37ab62a8eedb122461f73cc273476e3", size = 184936, upload-time = "2026-07-06T21:33:58.765Z" }, + { url = "https://files.pythonhosted.org/packages/e5/4b/1f4c36ab273980d7aa75bb126ea4f8971f24a96108acad3a0a084028c57b/cffi-2.1.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:cdf2448aab5f661c9315308ec8b93f4e8a1a67a3c733f8631067a2b67d5913dc", size = 185045, upload-time = "2026-07-06T21:34:00.085Z" }, + { url = "https://files.pythonhosted.org/packages/ef/c3/ad299dc38f3583f8d916b299f028af418a9ec98bc695fcbebeae7420691c/cffi-2.1.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:90bec57cf82089383bd06a605b3eb8daebf7e5a668520beaf6e327a83a947699", size = 222342, upload-time = "2026-07-06T21:34:01.814Z" }, + { url = "https://files.pythonhosted.org/packages/eb/d8/df4543cc087245044ed02ef3ad8e0a26619d0075ac7a77a12dc81177851b/cffi-2.1.0-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6274dcb2d15cef48daa73ed1be5a40d501d74dccd0cd6db364776d12cb6ba022", size = 210073, upload-time = "2026-07-06T21:34:03.255Z" }, + { url = "https://files.pythonhosted.org/packages/2c/0e/fac738d73728c6cea2a88a2883dca54892496cbba88a1dc1f2909cb8a6f5/cffi-2.1.0-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:2b71d409cccee78310ab5dec549aed052aaea483346e282c7b02362596e01bb0", size = 208551, upload-time = "2026-07-06T21:34:04.433Z" }, + { url = "https://files.pythonhosted.org/packages/e6/3f/0b04a700dd64f465c93020253a793a82c9b4dff9961f48facd0df945d9b8/cffi-2.1.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7d3538f9c0e50670f4deb93dbb696576e60590369cae2faf7de681e597a8a1f1", size = 221649, upload-time = "2026-07-06T21:34:06.157Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7c/b7379a5704c79eda57ce075869ba70a0368d1c850f803b3c0d078d39dcaf/cffi-2.1.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:8f9ec95b8a043d3dfbc74d9abc6f7baf524dd27a8dc160b0a32ff9cdab650c28", size = 225203, upload-time = "2026-07-06T21:34:07.489Z" }, + { url = "https://files.pythonhosted.org/packages/5a/02/d5e6c43ea85c41bda2a184a3418f195fe7cf602967a8d2b94e085b83deef/cffi-2.1.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:af5e2915d41fe6c961694d7bfdc8562942638200f3ce2765dfb8b745cf997629", size = 223263, upload-time = "2026-07-06T21:34:08.712Z" }, + { url = "https://files.pythonhosted.org/packages/2c/d8/772b8259bf75749adffb1c546828978381fb516f60cf701f6c83daf60c85/cffi-2.1.0-cp315-cp315-win32.whl", hash = "sha256:0a42c688d19fca6e095a53c6a6e2295a5b050a8b289f109adab02a9e61a25de6", size = 177696, upload-time = "2026-07-06T21:34:26.355Z" }, + { url = "https://files.pythonhosted.org/packages/2f/dd/afa2191fc6d57fedd26e5844a2fe2fcc0bbfa00961bbaa5a41e4921e7cca/cffi-2.1.0-cp315-cp315-win_amd64.whl", hash = "sha256:bccbbb5ee76a61f9d99b5bf3846a51d7fca4b6a732fe46f89295610edaf41853", size = 187914, upload-time = "2026-07-06T21:34:27.58Z" }, + { url = "https://files.pythonhosted.org/packages/05/ef/6cd4f8c671517162379dc79cfae5aea9106bc38abb89628d5c16adf6a838/cffi-2.1.0-cp315-cp315-win_arm64.whl", hash = "sha256:8d35c139744adb3e727cd51b1a18324bbe44b8bd41bf8322bca4d41289f48eda", size = 183004, upload-time = "2026-07-06T21:34:28.905Z" }, + { url = "https://files.pythonhosted.org/packages/11/b6/12fc55092817a5faa26fb8c40c7f9d662e11a46ee248c137aafc42517d92/cffi-2.1.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:f9912624a0c0b834b7520d7769b3644453aabc0a7e1c839da7359f050750e9bc", size = 188378, upload-time = "2026-07-06T21:34:09.926Z" }, + { url = "https://files.pythonhosted.org/packages/8d/2e/cdac88979f295fde5daa69622c7d2111e56e7ceb94f211357fbe452339e4/cffi-2.1.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:df92f2aba50eb4d96718b68ef76f2e57a57b54f2fa62333496d16c6d585a85ca", size = 188319, upload-time = "2026-07-06T21:34:11.101Z" }, + { url = "https://files.pythonhosted.org/packages/e0/27/1d0b408497e41a74795af122d7b603c418c5fed0171450f899afd04e594f/cffi-2.1.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0520e1f4c35f44e209cbbb421b67eec42e6a157f59444dfb6058874ff3610e5d", size = 223904, upload-time = "2026-07-06T21:34:12.606Z" }, + { url = "https://files.pythonhosted.org/packages/8b/31/e115c985105dd7ffb32444505f18ceb874bb42d992af05d5dced7ecf1980/cffi-2.1.0-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:3681e031db29958a7502f5c0c9d6bbc4c36cb20f7b104086fa642d1799631ff8", size = 211554, upload-time = "2026-07-06T21:34:13.987Z" }, + { url = "https://files.pythonhosted.org/packages/5a/67/9e6e09409336d9e515c58367e7cfcf4f89df06ad25252675595a58eb59d5/cffi-2.1.0-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:762f99479dcb369f60ab9017ad4ab97a36a1dd7c1ee5a3b15db0f4b8659120cd", size = 210795, upload-time = "2026-07-06T21:34:15.972Z" }, + { url = "https://files.pythonhosted.org/packages/19/e5/d3cc82a4a0be7902af279c04181ad038449c096734464a5ae1de3e1401bd/cffi-2.1.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0611e7ebf90573a535ebdc33ae9da222d037853983e13359f580fab781ca017f", size = 223843, upload-time = "2026-07-06T21:34:17.509Z" }, + { url = "https://files.pythonhosted.org/packages/b9/65/b434abc97ce7cecc2c640fde160507c0ecc7e21544b483ba3325d2e2ea17/cffi-2.1.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:86cf8755a791f72c85dc287128cc62d4f24d392e3f1e15837245623f4a33cccc", size = 226773, upload-time = "2026-07-06T21:34:19.05Z" }, + { url = "https://files.pythonhosted.org/packages/b5/9f/d4dc66ca651eb1145a133314cda721abf13cfac3d28c4a0402263ae6ad75/cffi-2.1.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:ba00f661f8ba35d075c937174e27c2c421cec3942fd2e0ea3e66996757c0fdd9", size = 225719, upload-time = "2026-07-06T21:34:20.576Z" }, + { url = "https://files.pythonhosted.org/packages/68/5a/e536c528bc8057496c360c0978559a2dc45653f89dd6151078aa7d8fca1a/cffi-2.1.0-cp315-cp315t-win32.whl", hash = "sha256:cb96698e3c7413d906ce83f8ffd245ec1bd94707541f299d0ce4d6b0193e982b", size = 182760, upload-time = "2026-07-06T21:34:22.059Z" }, + { url = "https://files.pythonhosted.org/packages/d3/0b/0ffe8b82d3875bced5fa1e7986a7a46b748262a40ab7f60b475eb9fb1bb3/cffi-2.1.0-cp315-cp315t-win_amd64.whl", hash = "sha256:f146d154428a2523f9cc7936c02353c2459b8f6cf07d3cd1ee1c0a611109c5d5", size = 193769, upload-time = "2026-07-06T21:34:23.589Z" }, + { url = "https://files.pythonhosted.org/packages/a0/17/1073b53b68c9b5ca6914adf5f8bf55aacc2d3be102418c90700160ea8605/cffi-2.1.0-cp315-cp315t-win_arm64.whl", hash = "sha256:cbb7640ce37159548d2147b5b8c241f962143d4c71231431820783f4dc78f210", size = 186405, upload-time = "2026-07-06T21:34:24.857Z" }, ] [[package]] name = "click" version = "8.4.2" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" } +source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/76/d4/81420972a676e8ffea40450d8c8c92943e7218a78fe9b64359836cc9876b/click-8.4.2.tar.gz", hash = "sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6", size = 338000, upload-time = "2026-06-24T17:45:15.148Z" } +sdist = { url = "https://files.pythonhosted.org/packages/76/d4/81420972a676e8ffea40450d8c8c92943e7218a78fe9b64359836cc9876b/click-8.4.2.tar.gz", hash = "sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6", size = 338000, upload-time = "2026-06-24T17:45:15.148Z" } wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl", hash = "sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76", size = 119243, upload-time = "2026-06-24T17:45:13.73Z" }, + { url = "https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl", hash = "sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76", size = 119243, upload-time = "2026-06-24T17:45:13.73Z" }, ] [[package]] name = "colorama" version = "0.4.6" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" } -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] [[package]] name = "comm" version = "0.2.3" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" } -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/4c/13/7d740c5849255756bc17888787313b61fd38a0a8304fc4f073dfc46122aa/comm-0.2.3.tar.gz", hash = "sha256:2dc8048c10962d55d7ad693be1e7045d891b7ce8d999c97963a5e3e99c055971", size = 6319, upload-time = "2025-07-25T14:02:04.452Z" } +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4c/13/7d740c5849255756bc17888787313b61fd38a0a8304fc4f073dfc46122aa/comm-0.2.3.tar.gz", hash = "sha256:2dc8048c10962d55d7ad693be1e7045d891b7ce8d999c97963a5e3e99c055971", size = 6319, upload-time = "2025-07-25T14:02:04.452Z" } wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl", hash = "sha256:c615d91d75f7f04f095b30d1c1711babd43bdc6419c1be9886a85f2f4e489417", size = 7294, upload-time = "2025-07-25T14:02:02.896Z" }, + { url = "https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl", hash = "sha256:c615d91d75f7f04f095b30d1c1711babd43bdc6419c1be9886a85f2f4e489417", size = 7294, upload-time = "2025-07-25T14:02:02.896Z" }, ] [[package]] name = "common" -version = "0.1.0" +version = "0.2.0" source = { editable = "common" } dependencies = [ - { name = "alembic" }, - { name = "pydantic-settings" }, - { name = "pymysql" }, + { name = "asyncmy" }, + { name = "boto3" }, + { name = "fastapi" }, { name = "sqlalchemy" }, ] [package.metadata] requires-dist = [ - { name = "alembic", specifier = ">=1.18.5" }, - { name = "pydantic-settings", specifier = ">=2.14.2" }, - { name = "pymysql", specifier = ">=1.2.0" }, - { name = "sqlalchemy", specifier = ">=2.0.51" }, + { name = "asyncmy", specifier = "==0.2.11" }, + { name = "boto3", specifier = ">=1.34,<2" }, + { name = "fastapi", specifier = "==0.116.1" }, + { name = "sqlalchemy", specifier = "==2.0.51" }, +] + +[[package]] +name = "croniter" +version = "6.2.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/37/57/2e2a65aee2a70483cb28e2b7e15a072d00a523207593b44400d4717bb100/croniter-6.2.4.tar.gz", hash = "sha256:fc124f751b1b04805c2a04b061898b436b45ab2320b045e1e052ea895de65189", size = 166267, upload-time = "2026-07-10T09:52:59.955Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cd/ba/d678e5bd329646ca51d3c92addbc77804e86d21f4b6b6a027218e6abb010/croniter-6.2.4-py3-none-any.whl", hash = "sha256:8ef3d544107a5c05a150a2d78f8bf5a8eb9c5c4d93405a736b824109574e3f4d", size = 46677, upload-time = "2026-07-10T09:52:58.425Z" }, +] + +[[package]] +name = "cryptography" +version = "49.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1f/99/d1c90d6041656cc6ee229dc99cd67fd0cd5aec3c5f7d72fffc27cc750054/cryptography-49.0.0.tar.gz", hash = "sha256:f89660a348f4f78a92366240a61404e337586ef7f5909a2fef59ca88ef505493", size = 854345, upload-time = "2026-06-12T20:02:30.512Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/22/adf66990e63584a68dfb50c24f48a125c07b1699899381c8151e63ed458c/cryptography-49.0.0-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:966fe0e9c67490071f14c0d2b1cb2dfb3023c5ce39457343931415f08382f2db", size = 4032100, upload-time = "2026-06-12T20:02:32.143Z" }, + { url = "https://files.pythonhosted.org/packages/09/41/3797cfaf69cae04a13ee78ebd83f0678d9c02b4779d21ce24445326f1a69/cryptography-49.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:36d1709f992593689b45bda411498d62c6e365f2ca00b84657d4dadd24de16db", size = 4692978, upload-time = "2026-06-12T20:01:21.305Z" }, + { url = "https://files.pythonhosted.org/packages/e6/8b/43011f7ebe515a8aa20d61f290a326cd890c2e738e16e59eaff8d9c3a412/cryptography-49.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0e959b578856a3924bc0cbb710fc12c387b9412a951389f3ca61704a9e25f325", size = 4716422, upload-time = "2026-06-12T20:01:48.566Z" }, + { url = "https://files.pythonhosted.org/packages/4a/91/01ce7303a4579e6d3a6abef01bd322848e9ea7a219adcabc5048b9033571/cryptography-49.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:53ecee2e23f7169b6117e99fc8a944e5e50f79e69758a83b52a00cb98ab2b2d2", size = 4700503, upload-time = "2026-06-12T20:02:47.091Z" }, + { url = "https://files.pythonhosted.org/packages/62/99/a2c95cf8293f07491e9e27c20cc4dcd18176d944e674679adeb1d0173fd6/cryptography-49.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:2eda353d8a27bcbcaa4cbed18994a74ab4d19a2ca897db188ea269ab9b71419b", size = 5309779, upload-time = "2026-06-12T20:02:08.987Z" }, + { url = "https://files.pythonhosted.org/packages/20/2c/0622f20ff02b2ef32558733443805dc82fd4c275be01b2d19d14676f3a1b/cryptography-49.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2afe9051da7ae7bd5905da5a949280c7d2bb75682e188f650a9d0f2756b834c6", size = 4749683, upload-time = "2026-06-12T20:02:03.335Z" }, + { url = "https://files.pythonhosted.org/packages/a3/5b/c5246635d5fd3b64e0d45ae10e99fd32fe9676a79915ccfe5a61ba9af1a5/cryptography-49.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:0b82e28ee398a386f0807bba7884d30f25218855690f45115831bcce5d90822c", size = 4337874, upload-time = "2026-06-12T20:02:54.323Z" }, + { url = "https://files.pythonhosted.org/packages/6d/88/05563c7fe2e914e87d1a536d06fe83e66b4e1d95cb593e05aea375531da8/cryptography-49.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:ccac2bfebc306b862133e3bb71f3f6ee8bb525240089b2d952e4144b3a6d5da7", size = 4700283, upload-time = "2026-06-12T20:01:34.822Z" }, + { url = "https://files.pythonhosted.org/packages/c4/b6/d7696e4e890d6ae1469935164c9e5215c557671cb78d6e3f458ccceaa632/cryptography-49.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:d0527ce944105f257f605a827d6ebead966c752038b6e8656abb9c5edee6fc68", size = 5265844, upload-time = "2026-06-12T20:01:24.09Z" }, + { url = "https://files.pythonhosted.org/packages/a9/3c/f3ad17eecc1a57b0ba236dc01f90e783c51f4a2f35f64777cc4f47a184b2/cryptography-49.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:cbc77da8c523d5abd028635ba850a6966fcee2c82e2bf65a41d1d8afe0f98be9", size = 4749290, upload-time = "2026-06-12T20:01:30.848Z" }, + { url = "https://files.pythonhosted.org/packages/4f/01/339573cf1023163a400b0b5d16f6d507de413b9f60be6fd1b77feeaf6737/cryptography-49.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b87e65d263b3e5d3bb92a57e2a6638e2f31110fa7aa890c7b2dbba42248d0a3f", size = 4834612, upload-time = "2026-06-12T20:01:29.246Z" }, + { url = "https://files.pythonhosted.org/packages/71/fd/577302e213a1be9468f92d1afef66fcf1ef83d516819d9992ca547f592bd/cryptography-49.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:66ec79c3904820572d7e987abdf304281f141d37ad9a489b8e97066e7b9b6459", size = 4980804, upload-time = "2026-06-12T20:01:42.853Z" }, + { url = "https://files.pythonhosted.org/packages/1f/09/f42b1d190c5ba75f72062a387f8030d1d75f6ab035788f1d9c4b01de6525/cryptography-49.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:e5dfc1e64de5677cec922ffa8da89c546d0415bf6efdf081842e5d44c84e1f0e", size = 3810026, upload-time = "2026-06-12T20:02:39.262Z" }, + { url = "https://files.pythonhosted.org/packages/ec/9e/db72b3ae7fc9cfad53e630e56c6ae83b9b6ff0bf3718ffb8012d20b3aabf/cryptography-49.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:73a205dce83953d131a4aa1e0fd917a2fd1c5b1eef251e9d7152efefcbf5caf7", size = 4013892, upload-time = "2026-06-12T20:02:10.735Z" }, + { url = "https://files.pythonhosted.org/packages/86/12/c48a424f38db03027be9f7ed5c7dc5de9933dbee992865f98b13727a009d/cryptography-49.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:196ecd6a36e4e9aa10270393bb98d8df88fccee0bf1e5128b91ae4eb4375896d", size = 4678835, upload-time = "2026-06-12T20:02:48.743Z" }, + { url = "https://files.pythonhosted.org/packages/68/28/8a3ad4653662c93fc44dc4e5d8fd374c25c42e07b34bbfbadf49cf57a5a8/cryptography-49.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7abcee80084cda3f7691f3eb1ce480d8df49cec637b429aa35986c1de71738aa", size = 4697239, upload-time = "2026-06-12T20:02:56.03Z" }, + { url = "https://files.pythonhosted.org/packages/a8/b2/2193fc74f81aee4f9b62733133b73b5176718932ed8f2e4b03fa040480a6/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:4ae387c9cb68ea569ca17e490d66d8142b81c3cc814bf179974b7d146e490bbb", size = 4685593, upload-time = "2026-06-12T20:02:50.666Z" }, + { url = "https://files.pythonhosted.org/packages/47/f1/1d3eaa243bfc5de4a187b22aa8c048b3e4980bfbe830ac46e6bac2e66947/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:f37d847238971164fdbc68ade6f6574aecc9c0af714190e2083429ff68f4ce9d", size = 5289961, upload-time = "2026-06-12T20:01:46.468Z" }, + { url = "https://files.pythonhosted.org/packages/58/39/2d51306721330c486495853eda1c567880ff036de15a14c4b74f399934af/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:c2bc30226390d60ea19d9f82b19db005fe0452154a23c1c410c12ea801e43561", size = 4731145, upload-time = "2026-06-12T20:02:16.832Z" }, + { url = "https://files.pythonhosted.org/packages/17/50/983e838c7fd0d87fd8c969bcdd328edaf5f756e38df5281637424c155873/cryptography-49.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:07cab27cc7b7e0fd28e5e26bb9eeedde5c135c868b46de4a27845abe94af6122", size = 4321719, upload-time = "2026-06-12T20:02:52.611Z" }, + { url = "https://files.pythonhosted.org/packages/a7/f5/8f571d7e27c55bce9f76f026143bcb1e040a4233149ecca0bea5fa5dd5f7/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:b20133d204d2bb56ba047642199603876c872026ca53e79c35b83772ab2cc505", size = 4685209, upload-time = "2026-06-12T20:02:07.282Z" }, + { url = "https://files.pythonhosted.org/packages/e7/84/0e27016a6fc5a0886f797018b26aa42f40c09a82332bff77822a451deaaa/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:b970c6da94d5bb18629db453d14f2a1300f6bf59b61e9b82377931ef95504866", size = 5246285, upload-time = "2026-06-12T20:01:32.439Z" }, + { url = "https://files.pythonhosted.org/packages/11/2d/5e1fb307cb5931881516b464c98774b3f2c36b5d4bb9a2830253cf553cad/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:d8ecde755e2e91bf773fc94e8c9d730cd7f2007004cb492263a794ec3899a1c8", size = 4730441, upload-time = "2026-06-12T20:02:01.469Z" }, + { url = "https://files.pythonhosted.org/packages/e4/c0/bff5a02ee731d207d6a1ed51732549d8c53d2bc8da1d10ec6f2844201d68/cryptography-49.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e3fb64c420688e5319ae25113a354015abbd8dffbfbc41781a1ea66fc7622ac3", size = 4815869, upload-time = "2026-06-12T20:01:36.574Z" }, + { url = "https://files.pythonhosted.org/packages/b9/26/814681d14248d95d73d5c3eea0c39a94eb8302df966f670a2c60de90974b/cryptography-49.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32703d93296f5c1f4b53349ad3a250c2cae0fdecd3a3dd5d47e616d8d616af27", size = 4960948, upload-time = "2026-06-12T20:02:18.688Z" }, + { url = "https://files.pythonhosted.org/packages/4c/fe/93ecac273d3738939d023612ad12cca9a3740a5345d69fda04134c43fd96/cryptography-49.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:33cd0565932807baddb67b96dbee92f2c374b5c89dee09fd74079aeb8c8dba61", size = 3799153, upload-time = "2026-06-12T20:01:39.059Z" }, + { url = "https://files.pythonhosted.org/packages/19/2a/5bb823f5bedcf80718cea7fbc95ec5515cca3769633c4b01a32be7f30e7c/cryptography-49.0.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ec5e529fb80935c94fe7b729f9972b50e351a0e6b50aa294fd5cabb109fcc29a", size = 4025947, upload-time = "2026-06-12T20:01:25.745Z" }, + { url = "https://files.pythonhosted.org/packages/3d/df/40577043ca124e17012f408ddddaeb213b856336ac82ddb3bc915f39e29f/cryptography-49.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f78ff2c9ed8dc2d036b0f4d640e22522213d047c1b14e61205a7e55c80a494d4", size = 4692429, upload-time = "2026-06-12T20:01:53.628Z" }, + { url = "https://files.pythonhosted.org/packages/2c/99/2d13299eb3dd27b02dcfaafcc91d6b5cb3329f7cbd6d8f51921acd566c1a/cryptography-49.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:35b151772baff2c74cba7fa290ceaff4c3b11c0c881eb93eb5dbc05a7cfbba18", size = 4700968, upload-time = "2026-06-12T20:02:45.383Z" }, + { url = "https://files.pythonhosted.org/packages/a5/4d/9c0cd02f95e2602dd5e563da149ee0830abef3537be8b34dc56281ebe27a/cryptography-49.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:0f21641cf4b30fca7aee061ced0ec7ad7b073518088b7c9969a297c0ae796c69", size = 4697758, upload-time = "2026-06-12T20:01:41.13Z" }, + { url = "https://files.pythonhosted.org/packages/24/01/186c825898477d77e2324d5360fefe622ff1d8d1963ec0554e2cada8ec77/cryptography-49.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:9e82dcc8e56052715fb18b2429e3bca4823b1629136a2084fc45a9a5cecb9b64", size = 5298863, upload-time = "2026-06-12T20:02:24.579Z" }, + { url = "https://files.pythonhosted.org/packages/b8/7b/62cbbab75d0659865bf0273790031544a0b16c8072d258f9428dcd8190dc/cryptography-49.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:6f2debedf9ca60cf1d5bd466475638af5130f89965605cd818484d19987d3a21", size = 4735983, upload-time = "2026-06-12T20:01:50.14Z" }, + { url = "https://files.pythonhosted.org/packages/6c/72/3e798c064bc39e471008075d0f9bc9daf77a80879c092e4a8e170c585ed4/cryptography-49.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:8c25ceb16df5b9435f3f6a9829204985b0e0cbee3b48aacd432c7d2c850b44d9", size = 4334173, upload-time = "2026-06-12T20:01:44.743Z" }, + { url = "https://files.pythonhosted.org/packages/f0/ee/6fca21d1ac73e06f8bef71940abfd4d2f6472b4bca284d770f32bd4086f6/cryptography-49.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:28d8b15e6275f12c8a207dc309dfa957903c927d08d0cc937ee3f63f200693cc", size = 4697298, upload-time = "2026-06-12T20:02:20.918Z" }, + { url = "https://files.pythonhosted.org/packages/67/d0/a5fcd3515f0bae49a7b6d0413cc1bdccdcc1fc0047037a0d480642cdc5d6/cryptography-49.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:6fc361c34fb6aac015ce19435876635e5c6d21db31998b0920f675f131e043b8", size = 5254338, upload-time = "2026-06-12T20:02:22.737Z" }, + { url = "https://files.pythonhosted.org/packages/a0/84/84fe36f19caf857d61cb7fc9c63035a47ffabd84ea12d1d393148efa3615/cryptography-49.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:2400ef9c9e2299a25614eb1dea3db54a69b1349efd043bfac9c67630d136df36", size = 4735650, upload-time = "2026-06-12T20:02:41.389Z" }, + { url = "https://files.pythonhosted.org/packages/6c/a0/db537264e234f7273a73ec020873d6d6b39dfd8a53db78b550ca8320440e/cryptography-49.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:67e1d20ad9ef3a563c59ef22e7a8a0b8210bd26604369ea4a30a7c66aefe504e", size = 4834820, upload-time = "2026-06-12T20:01:51.847Z" }, + { url = "https://files.pythonhosted.org/packages/93/77/8df9eb486495979bccecd1062e2eaf435250e84437040295b57d09048b0b/cryptography-49.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:42b0684e0e40cf26122427802486f6d93aea593612603a94fbf260c7eb1e9c1b", size = 4967968, upload-time = "2026-06-12T20:02:12.524Z" }, + { url = "https://files.pythonhosted.org/packages/c2/e6/f60198ea8d9dfa15fff9ed4ca02ce362f6eadd9ba757dcc50634c4257b63/cryptography-49.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:026ac7423e6fa66872d3bf889be5974507da3944f866f704fa200eadacd00001", size = 3785547, upload-time = "2026-06-12T20:02:26.847Z" }, ] [[package]] name = "debugpy" version = "1.8.21" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" } -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f2/aa/12037145b7a56eaa5b29b41872f7a21b538e807e13f32c4d3c46e59be084/debugpy-1.8.21.tar.gz", hash = "sha256:a3c53278e84c94e11bd87c53970ec391d1a67396c8b22609fcac576520e611a6", size = 1697577, upload-time = "2026-06-01T19:30:35.156Z" } +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/aa/12037145b7a56eaa5b29b41872f7a21b538e807e13f32c4d3c46e59be084/debugpy-1.8.21.tar.gz", hash = "sha256:a3c53278e84c94e11bd87c53970ec391d1a67396c8b22609fcac576520e611a6", size = 1697577, upload-time = "2026-06-01T19:30:35.156Z" } wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a2/df/bf625547431a9cadc9f4cbfeda38866e2b17f6aed147b625377e87834449/debugpy-1.8.21-cp312-cp312-macosx_15_0_universal2.whl", hash = "sha256:9f96713896f39c3dff0ee841f47320c3f2983d33c341e009361bb0ebc79adc4e", size = 2483609, upload-time = "2026-06-01T19:30:50.794Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/bf/09/59324b903599031ff9faaec1758292409f6561a0ec2492fe4b703327705a/debugpy-1.8.21-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:c193d474f0a211191f2b4449d2d06157c689013035bd952f3b617e0ef422b176", size = 3968900, upload-time = "2026-06-01T19:30:52.341Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/14/cd/27f65b805d7fe005c44e1a36b9183ecdfbcdbf9d3e721a5115d461ecc7ee/debugpy-1.8.21-cp312-cp312-win32.whl", hash = "sha256:4743373c1cac7f9e74a1b9915bf1dbe0e900eca657ffb170ae07ac8363205ae9", size = 5336340, upload-time = "2026-06-01T19:30:54.047Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/77/1d/c84e30c0c674184948b66f076ab271c01d940618a2824c23cd035a27bc20/debugpy-1.8.21-cp312-cp312-win_amd64.whl", hash = "sha256:bd7ba9dd3daa7c2f942c6ca8d4695a16bf9ac16b63615261c7982bc74f7ed20c", size = 5374751, upload-time = "2026-06-01T19:30:55.891Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/77/6b/d817e1f8cc77aa055d37fba092e0febfdff40fe652d8d53d4cd7a86ad98d/debugpy-1.8.21-cp313-cp313-macosx_15_0_universal2.whl", hash = "sha256:13678151fc401e2d68c9880b91e28714f797d40422994572b24560ef80910a88", size = 2477398, upload-time = "2026-06-01T19:30:57.644Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/48/57/412421516afc3055fa577516f00beec3d663f9b0ab330639547ae6c57720/debugpy-1.8.21-cp313-cp313-manylinux_2_34_x86_64.whl", hash = "sha256:ecbd158386c31ffe71d46f72d44d56e66331ab9b16cad649156d514368f23ab2", size = 3962096, upload-time = "2026-06-01T19:30:59.235Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c1/62/2c616337cf6ba7b07ebbc97f02c6c945a8e2f76b365e33ee809c32ee36d1/debugpy-1.8.21-cp313-cp313-win32.whl", hash = "sha256:2c2ae706dec41d99a9ca1f7ebc987a83e65578363be6f6b3ac9067504917fae1", size = 5336288, upload-time = "2026-06-01T19:31:00.79Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f8/99/9175103392f84c4b1bf7622888cdc68da07f0ff7d9e581266428f6776033/debugpy-1.8.21-cp313-cp313-win_amd64.whl", hash = "sha256:aa648733047443eb1d07682c4ef287d36a54507b643ffdf38b09a3ef002c72a0", size = 5376567, upload-time = "2026-06-01T19:31:02.56Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ce/3d/f4bbb323a548bfab2af3d6b4ffd9bf22636e55956a1285d317a1de643aad/debugpy-1.8.21-cp314-cp314-macosx_15_0_universal2.whl", hash = "sha256:9bb2a685287a2ac9b181cde89edcec64845cb51de7faaa75badb9a698bc24782", size = 2477209, upload-time = "2026-06-01T19:31:04.157Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/8c/2d/6e7ec524984a1702777868de49a4c53202bddac2a432a76a093469587750/debugpy-1.8.21-cp314-cp314-manylinux_2_34_x86_64.whl", hash = "sha256:3d6922439bf33fd38a3e2c447869ebc7b97da5cd3d329ff1ef9bc06c4903437e", size = 3927115, upload-time = "2026-06-01T19:31:05.863Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/97/47/d1aa6d64005a98a9144647d99306b419396f9ad7bf1d73c119e17a81fb4d/debugpy-1.8.21-cp314-cp314-win32.whl", hash = "sha256:15d4963bd5ffa48f0da0947fd06757fa7621945048a14ad7705431566d3c0e7c", size = 5336724, upload-time = "2026-06-01T19:31:07.711Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/5f/67/b905b90d163af11878c1af8abafa4a25206335e112e284e413454543a6da/debugpy-1.8.21-cp314-cp314-win_amd64.whl", hash = "sha256:fe0744a12353406de0ae8ccff0d0a4a666f00801a3db8fd04e7a5f761cd520e8", size = 5373803, upload-time = "2026-06-01T19:31:09.469Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/95/51/67e7cf11a53e40694f720457d5b3a1cdaaa3d5a9a633e482f225456b93ff/debugpy-1.8.21-py2.py3-none-any.whl", hash = "sha256:b1e37d333663c8851516a47364ef473da127f9caebe4417e6df6f5825a7e9a92", size = 5352888, upload-time = "2026-06-01T19:31:25.186Z" }, + { url = "https://files.pythonhosted.org/packages/a2/df/bf625547431a9cadc9f4cbfeda38866e2b17f6aed147b625377e87834449/debugpy-1.8.21-cp312-cp312-macosx_15_0_universal2.whl", hash = "sha256:9f96713896f39c3dff0ee841f47320c3f2983d33c341e009361bb0ebc79adc4e", size = 2483609, upload-time = "2026-06-01T19:30:50.794Z" }, + { url = "https://files.pythonhosted.org/packages/bf/09/59324b903599031ff9faaec1758292409f6561a0ec2492fe4b703327705a/debugpy-1.8.21-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:c193d474f0a211191f2b4449d2d06157c689013035bd952f3b617e0ef422b176", size = 3968900, upload-time = "2026-06-01T19:30:52.341Z" }, + { url = "https://files.pythonhosted.org/packages/14/cd/27f65b805d7fe005c44e1a36b9183ecdfbcdbf9d3e721a5115d461ecc7ee/debugpy-1.8.21-cp312-cp312-win32.whl", hash = "sha256:4743373c1cac7f9e74a1b9915bf1dbe0e900eca657ffb170ae07ac8363205ae9", size = 5336340, upload-time = "2026-06-01T19:30:54.047Z" }, + { url = "https://files.pythonhosted.org/packages/77/1d/c84e30c0c674184948b66f076ab271c01d940618a2824c23cd035a27bc20/debugpy-1.8.21-cp312-cp312-win_amd64.whl", hash = "sha256:bd7ba9dd3daa7c2f942c6ca8d4695a16bf9ac16b63615261c7982bc74f7ed20c", size = 5374751, upload-time = "2026-06-01T19:30:55.891Z" }, + { url = "https://files.pythonhosted.org/packages/77/6b/d817e1f8cc77aa055d37fba092e0febfdff40fe652d8d53d4cd7a86ad98d/debugpy-1.8.21-cp313-cp313-macosx_15_0_universal2.whl", hash = "sha256:13678151fc401e2d68c9880b91e28714f797d40422994572b24560ef80910a88", size = 2477398, upload-time = "2026-06-01T19:30:57.644Z" }, + { url = "https://files.pythonhosted.org/packages/48/57/412421516afc3055fa577516f00beec3d663f9b0ab330639547ae6c57720/debugpy-1.8.21-cp313-cp313-manylinux_2_34_x86_64.whl", hash = "sha256:ecbd158386c31ffe71d46f72d44d56e66331ab9b16cad649156d514368f23ab2", size = 3962096, upload-time = "2026-06-01T19:30:59.235Z" }, + { url = "https://files.pythonhosted.org/packages/c1/62/2c616337cf6ba7b07ebbc97f02c6c945a8e2f76b365e33ee809c32ee36d1/debugpy-1.8.21-cp313-cp313-win32.whl", hash = "sha256:2c2ae706dec41d99a9ca1f7ebc987a83e65578363be6f6b3ac9067504917fae1", size = 5336288, upload-time = "2026-06-01T19:31:00.79Z" }, + { url = "https://files.pythonhosted.org/packages/f8/99/9175103392f84c4b1bf7622888cdc68da07f0ff7d9e581266428f6776033/debugpy-1.8.21-cp313-cp313-win_amd64.whl", hash = "sha256:aa648733047443eb1d07682c4ef287d36a54507b643ffdf38b09a3ef002c72a0", size = 5376567, upload-time = "2026-06-01T19:31:02.56Z" }, + { url = "https://files.pythonhosted.org/packages/ce/3d/f4bbb323a548bfab2af3d6b4ffd9bf22636e55956a1285d317a1de643aad/debugpy-1.8.21-cp314-cp314-macosx_15_0_universal2.whl", hash = "sha256:9bb2a685287a2ac9b181cde89edcec64845cb51de7faaa75badb9a698bc24782", size = 2477209, upload-time = "2026-06-01T19:31:04.157Z" }, + { url = "https://files.pythonhosted.org/packages/8c/2d/6e7ec524984a1702777868de49a4c53202bddac2a432a76a093469587750/debugpy-1.8.21-cp314-cp314-manylinux_2_34_x86_64.whl", hash = "sha256:3d6922439bf33fd38a3e2c447869ebc7b97da5cd3d329ff1ef9bc06c4903437e", size = 3927115, upload-time = "2026-06-01T19:31:05.863Z" }, + { url = "https://files.pythonhosted.org/packages/97/47/d1aa6d64005a98a9144647d99306b419396f9ad7bf1d73c119e17a81fb4d/debugpy-1.8.21-cp314-cp314-win32.whl", hash = "sha256:15d4963bd5ffa48f0da0947fd06757fa7621945048a14ad7705431566d3c0e7c", size = 5336724, upload-time = "2026-06-01T19:31:07.711Z" }, + { url = "https://files.pythonhosted.org/packages/5f/67/b905b90d163af11878c1af8abafa4a25206335e112e284e413454543a6da/debugpy-1.8.21-cp314-cp314-win_amd64.whl", hash = "sha256:fe0744a12353406de0ae8ccff0d0a4a666f00801a3db8fd04e7a5f761cd520e8", size = 5373803, upload-time = "2026-06-01T19:31:09.469Z" }, + { url = "https://files.pythonhosted.org/packages/95/51/67e7cf11a53e40694f720457d5b3a1cdaaa3d5a9a633e482f225456b93ff/debugpy-1.8.21-py2.py3-none-any.whl", hash = "sha256:b1e37d333663c8851516a47364ef473da127f9caebe4417e6df6f5825a7e9a92", size = 5352888, upload-time = "2026-06-01T19:31:25.186Z" }, ] [[package]] name = "decorator" version = "5.3.1" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" } -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/60/8b/32f9823da46cde7df2087faa08cd98d01b908f8dcab982cdba9c84e85355/decorator-5.3.1.tar.gz", hash = "sha256:4cbcdd55a6efadb9dbea26b858f4fb3264567b52d69ca0d25b721b553f60ea82", size = 58084, upload-time = "2026-05-18T06:03:28.057Z" } +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/60/8b/32f9823da46cde7df2087faa08cd98d01b908f8dcab982cdba9c84e85355/decorator-5.3.1.tar.gz", hash = "sha256:4cbcdd55a6efadb9dbea26b858f4fb3264567b52d69ca0d25b721b553f60ea82", size = 58084, upload-time = "2026-05-18T06:03:28.057Z" } wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/05/7f/798705f5296a58ca505d600456748d1be48078eac8a7050d8a98bc9edb89/decorator-5.3.1-py3-none-any.whl", hash = "sha256:f47fe6fdbd2edd623ecfe36875d37aba411624e2670dd395dddae1358689bb3c", size = 10365, upload-time = "2026-05-18T06:03:26.517Z" }, -] - -[[package]] -name = "defusedxml" -version = "0.7.1" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" } -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/0f/d5/c66da9b79e5bdb124974bfe172b4daf3c984ebd9c2a06e2b8a4dc7331c72/defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69" } -wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61" }, + { url = "https://files.pythonhosted.org/packages/05/7f/798705f5296a58ca505d600456748d1be48078eac8a7050d8a98bc9edb89/decorator-5.3.1-py3-none-any.whl", hash = "sha256:f47fe6fdbd2edd623ecfe36875d37aba411624e2670dd395dddae1358689bb3c", size = 10365, upload-time = "2026-05-18T06:03:26.517Z" }, ] [[package]] name = "executing" version = "2.2.1" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" } -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/cc/28/c14e053b6762b1044f34a13aab6859bbf40456d37d23aa286ac24cfd9a5d/executing-2.2.1.tar.gz", hash = "sha256:3632cc370565f6648cc328b32435bd120a1e4ebb20c77e3fdde9a13cd1e533c4", size = 1129488, upload-time = "2025-09-01T09:48:10.866Z" } +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cc/28/c14e053b6762b1044f34a13aab6859bbf40456d37d23aa286ac24cfd9a5d/executing-2.2.1.tar.gz", hash = "sha256:3632cc370565f6648cc328b32435bd120a1e4ebb20c77e3fdde9a13cd1e533c4", size = 1129488, upload-time = "2025-09-01T09:48:10.866Z" } wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl", hash = "sha256:760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017", size = 28317, upload-time = "2025-09-01T09:48:08.5Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl", hash = "sha256:760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017", size = 28317, upload-time = "2025-09-01T09:48:08.5Z" }, ] [[package]] name = "fastapi" -version = "0.140.0" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" } +version = "0.116.1" +source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "annotated-doc" }, { name = "pydantic" }, { name = "starlette" }, { name = "typing-extensions" }, - { name = "typing-inspection" }, ] -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/0d/fb/fd7671137d9fa3df1d93a2f5111eb982709201724b29f211e4beb2d58688/fastapi-0.140.0.tar.gz", hash = "sha256:f338951b82fd74ca8f843163aec43ea1a1ce84d515415a50fa98fa25572a5544", size = 420968, upload-time = "2026-07-24T21:16:41.187Z" } +sdist = { url = "https://files.pythonhosted.org/packages/78/d7/6c8b3bfe33eeffa208183ec037fee0cce9f7f024089ab1c5d12ef04bd27c/fastapi-0.116.1.tar.gz", hash = "sha256:ed52cbf946abfd70c5a0dccb24673f0670deeb517a88b3544d03c2a6bf283143", size = 296485, upload-time = "2025-07-11T16:22:32.057Z" } wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/eb/76/6d9e25ad88da9d3ff744bcdbec4736e38c2288611d43f673a5d9bfa27c07/fastapi-0.140.0-py3-none-any.whl", hash = "sha256:e951c0a0d9540bf5d9a2a9e078fd415da2ab7e312d435139e7d9e2e7fe9f0b23", size = 130863, upload-time = "2026-07-24T21:16:42.89Z" }, + { url = "https://files.pythonhosted.org/packages/e5/47/d63c60f59a59467fda0f93f46335c9d18526d7071f025cb5b89d5353ea42/fastapi-0.116.1-py3-none-any.whl", hash = "sha256:c46ac7c312df840f0c9e220f7964bada936781bc4e2e6eb71f1c4d7553786565", size = 95631, upload-time = "2025-07-11T16:22:30.485Z" }, ] [[package]] name = "fastjsonschema" version = "2.22.0" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" } -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f1/11/c802f752919c1fd83d44b3b955c6e7120c79f355d4a448c358198a11178c/fastjsonschema-2.22.0.tar.gz", hash = "sha256:6eb12e8f9900db6166c3d396d178ebdf6a4215fe22a06e19792edd612a20035a", size = 382291, upload-time = "2026-07-25T20:32:35.561Z" } +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/11/c802f752919c1fd83d44b3b955c6e7120c79f355d4a448c358198a11178c/fastjsonschema-2.22.0.tar.gz", hash = "sha256:6eb12e8f9900db6166c3d396d178ebdf6a4215fe22a06e19792edd612a20035a", size = 382291, upload-time = "2026-07-25T20:32:35.561Z" } wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a4/9d/4a0f9355ca3e540b2d22d5f269212e2f227b8f277835b02d8908355245d1/fastjsonschema-2.22.0-py3-none-any.whl", hash = "sha256:60f4c92fda6f93efe3b3261638836478e1e11abc01c647e36e478199f7a86a37", size = 26248, upload-time = "2026-07-25T20:32:33.616Z" }, -] - -[[package]] -name = "fqdn" -version = "1.5.1" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" } -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/30/3e/a80a8c077fd798951169626cde3e239adeba7dab75deb3555716415bd9b0/fqdn-1.5.1.tar.gz", hash = "sha256:105ed3677e767fb5ca086a0c1f4bb66ebc3c100be518f0e0d755d9eae164d89f", size = 6015, upload-time = "2021-03-11T07:16:29.08Z" } -wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/cf/58/8acf1b3e91c58313ce5cb67df61001fc9dcd21be4fadb76c1a2d540e09ed/fqdn-1.5.1-py3-none-any.whl", hash = "sha256:3a179af3761e4df6eb2e026ff9e1a3033d3587bf980a0b1b2e1e5d08d7358014", size = 9121, upload-time = "2021-03-11T07:16:28.351Z" }, + { url = "https://files.pythonhosted.org/packages/a4/9d/4a0f9355ca3e540b2d22d5f269212e2f227b8f277835b02d8908355245d1/fastjsonschema-2.22.0-py3-none-any.whl", hash = "sha256:60f4c92fda6f93efe3b3261638836478e1e11abc01c647e36e478199f7a86a37", size = 26248, upload-time = "2026-07-25T20:32:33.616Z" }, ] [[package]] name = "greenlet" version = "3.5.4" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" } -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a3/74/b13368064b09053253555d3f2839cc2684d22d5aed0d2ccffbf7a6736558/greenlet-3.5.4.tar.gz", hash = "sha256:0232ae1de90a8e07867bb127d7a6ba2301e859145489f25cda8a6096dabe1d20", size = 206538, upload-time = "2026-07-22T12:47:14.468Z" } +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/74/b13368064b09053253555d3f2839cc2684d22d5aed0d2ccffbf7a6736558/greenlet-3.5.4.tar.gz", hash = "sha256:0232ae1de90a8e07867bb127d7a6ba2301e859145489f25cda8a6096dabe1d20", size = 206538, upload-time = "2026-07-22T12:47:14.468Z" } wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f3/04/81bd731d6d1e3a469d9a4c36f5eb069bcf0cbb2d5d342c9fec22245b91fc/greenlet-3.5.4-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:3d66250e8b09f182ede05490998c818b5961f7a3640332d44c4927caec7bbfe4", size = 295909, upload-time = "2026-07-22T11:38:09.261Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/cc/dd/f5f22903a6ae70f5ea328ed0beaec92ad903f0e3b7d2845133b354abc4b8/greenlet-3.5.4-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c90e930c9c192e5b3ee9fb8bcd920ea3926155e2e3ded39fc697323addecee17", size = 612011, upload-time = "2026-07-22T12:26:40.69Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/8e/10/92a4a88d12b915d74ea5b6d288e4afefda4771647caa34442c156f7a454f/greenlet-3.5.4-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:791fdfeeb9c6e0c7b10fa151bf110d2a6974866f13dcb5b1c7efae698245893a", size = 624299, upload-time = "2026-07-22T12:29:02.089Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/50/6d/0b14bb9db2989f32cd9fe7f76afedea01ee8bee3f87c07e69f24adfe7e63/greenlet-3.5.4-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f88193799d43dbf8c8a806d6405c9c52fe2af40bf75072a606357b33cc336c7f", size = 621541, upload-time = "2026-07-22T11:51:09.464Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/48/3d/25e9a2d9eb6b2e8b7ca4e80a3a26cb887cce6c8e0a87c921164f11bc5574/greenlet-3.5.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b7a5f095767c4493afcd06067f2bb3b8716e3f3f9e92b99c88e7e99f885b3d4d", size = 1581444, upload-time = "2026-07-22T12:25:03.818Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b9/96/4c9bf2e2c408dcc0556edce69efa9f802e82223573c53240136a086821f1/greenlet-3.5.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:42afdc1ab5f66da8c586c32af9224a74a706b4f0ea0dc3a4188a0860a09c65c9", size = 1645842, upload-time = "2026-07-22T11:51:12.295Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b5/41/303ecb26a3a56122c0f4d4073ee078881847bd6b6f463ae0ec57ec20223b/greenlet-3.5.4-cp312-cp312-win_amd64.whl", hash = "sha256:60149df8f462d1b230038e6590c23c3b4768bb5d6c022b3b6e82532b34b0b8a3", size = 247169, upload-time = "2026-07-22T11:38:19.893Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a4/e3/ef56864b4c35fcb3eb3b41b869f6cc46f4cd3f5e2c68e74acde8ac433951/greenlet-3.5.4-cp312-cp312-win_arm64.whl", hash = "sha256:77d6ce04fed0d9aeed42e0f37923cc43eba9b027bdd9c34546bb4ccd143d0fe0", size = 245565, upload-time = "2026-07-22T11:38:27.061Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c0/9a/e51225dcd58713f16ccbdcc501a8da21098ea14515b7870f1f94459e5ff5/greenlet-3.5.4-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:24e61b88cb7e1b1d794b32a10cc346ac779681d6d74ff137a3e0a444d2bf1f02", size = 294831, upload-time = "2026-07-22T11:38:53.389Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/9f/ea/de50a50fadf979713ab18b46f22ad5ff5f2dcfc637a3ebdecf669801e1a5/greenlet-3.5.4-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:870d730fec833f5a06906a32596cc099b9161594642a92a520b7a88911c95356", size = 614619, upload-time = "2026-07-22T12:26:42.282Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/db/c7/2aae27fea41205b8650294c301f042a2a4bb6155eea48c995b890a92f2c1/greenlet-3.5.4-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ec5ff0d1878df6af3bf9b638a5a92a7d5693291de77c91bff10fa48519c604ef", size = 627021, upload-time = "2026-07-22T12:29:03.445Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/eb/56/79fd826f9ccaae0b84e1b4ef68dabba5e105bb044ffcd448a0b782fcba9a/greenlet-3.5.4-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d84d993f6e575c950d91a23c1345d18fe1a4310d447bf630849d7809196b52f0", size = 624002, upload-time = "2026-07-22T11:51:11.391Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/0a/1a/27319f97e731298513dcba1a2e91b63e9d8811d9de22130f960b129b1bf1/greenlet-3.5.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:58023945f421093de5e6fa108c0985a8659d43f49e0216da25099369a121bcbd", size = 1581533, upload-time = "2026-07-22T12:25:05.322Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b1/6d/24240bf562e9786dd2799ee0a4a4dadb4ded22510f41b20245099159ac8c/greenlet-3.5.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bae2728e1897aa8df8cb1af38cd48b3a743aefe29372de7b8b7a9f532501e69f", size = 1645781, upload-time = "2026-07-22T11:51:14.805Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c1/5a/442ab1a9ef7ca6bf7210e5397a95972206a91a31033a03c8900866a10039/greenlet-3.5.4-cp313-cp313-win_amd64.whl", hash = "sha256:ca5726c0b08ca35ae873557266a78b2c3f3b2b7d7401aa5ff886c2045dd0111c", size = 247133, upload-time = "2026-07-22T11:39:20.661Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/3e/e6/9160210222386b1a378ff94db846b9508ca24a121cf684991561fdb69280/greenlet-3.5.4-cp313-cp313-win_arm64.whl", hash = "sha256:7c1303791d603080cac6fc3b34df51c3b75b723739c282c8029e48a0d241672f", size = 245500, upload-time = "2026-07-22T11:40:22.185Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a5/a7/6ab1d4f9cd548d15ab90da29947f2076100130bb179b0bde59f795a459e3/greenlet-3.5.4-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:7e8afa5eac028f8140ceafe5ceec66e6aa127ddcb21452d2a564dcd2900b5f22", size = 295410, upload-time = "2026-07-22T11:40:35.747Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/cd/7a/422f63b4715cbc0b24385305407adf38b48f6bb68b3e6b04090e994d0f5a/greenlet-3.5.4-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:73b37afe369021423ea53dd3123e04bffa7e93ac64429b9f50835b2e4fcae7cf", size = 661286, upload-time = "2026-07-22T12:26:43.8Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d0/31/5a1cac663bf5582190c5a714ef81364f03cde232227f39748f8ae4c11da5/greenlet-3.5.4-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3ef964f56dfcb6f9bbef2a190d9126795eac408716aeae47b5e7c73c32aafca9", size = 673517, upload-time = "2026-07-22T12:29:04.815Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/15/4a/2a82a1e3f8aaca020853ac8d12211280ca2b231aa08ea39f636f1060c319/greenlet-3.5.4-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c53ff01a5c53a40f2c16820ebc56d7c61a77f5fbe009dadd96292d5682f80f8", size = 670917, upload-time = "2026-07-22T11:51:13.589Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/68/b0/e379a152b17bfdfa95795af4049e37c0fd1b4d81f020d426db104ed07c77/greenlet-3.5.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ecca4d80d55a01ad6b23b33262662956149fbb7b2c6be2910f1705921958cbf3", size = 1628478, upload-time = "2026-07-22T12:25:06.678Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/5e/43/bffdfa64f7317f954c5c1230b5dd5922676ce198689a68c1ac1ed4b1b1a5/greenlet-3.5.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2ffbc533e0eaf8e80d8471411646ab88fe58f641d508c0b02b24494479f4d9ec", size = 1692021, upload-time = "2026-07-22T11:51:17.008Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d0/11/f799f9637e2c6e9b0b716015e339040598b058cf7654dfc0d67468b177ed/greenlet-3.5.4-cp314-cp314-win_amd64.whl", hash = "sha256:305f69e6c4523d7f6979ed001cff4e5853c063e5da04880296603aa0227e544c", size = 248031, upload-time = "2026-07-22T11:40:11.007Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/05/75/625bcdd74d5e6b2dca1ecba3c3ac77bcf8a026c21a649a46cef23e421f97/greenlet-3.5.4-cp314-cp314-win_arm64.whl", hash = "sha256:f260930bbbbcf9caee661211235a5111c86dfe5832fdf6ae4570da1e0995320f", size = 246892, upload-time = "2026-07-22T11:40:27.357Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ec/69/35c62ed49c320cb4d98e14698ccca5467d3bfe683984172be9cb564d9ce3/greenlet-3.5.4-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:41ddab54e4b238f4a6c323f39b4e59e176affd5a94d461a9fb7583dac74240a3", size = 305571, upload-time = "2026-07-22T11:40:31.659Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/5b/6c/64d60216b3640dcb0b62d913dd9e0d80030c09115bb2e4ba70c95d10ca45/greenlet-3.5.4-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b3dabe3e2809013052c68bdf0b7fa5f5f2859c43a80803131ad61af9cabd7867", size = 672568, upload-time = "2026-07-22T12:26:45.298Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/5c/de/ba3ab0a96292e53039530333b0d2ae18d9e508f3a325cd7bf15f8172944c/greenlet-3.5.4-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:27d3f00718634d4520a3a150154ac5da36f257869d41321953375b90bfbbc72c", size = 680076, upload-time = "2026-07-22T12:29:06.125Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a5/be/aeada79083c6f1c15f45d77a332f9c441af263ee298e3eb17522cd337d22/greenlet-3.5.4-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cbd60b5763c6543c1827e48faaf14ea9bfbad245f52b1a4d76a2a2d8884c6c66", size = 676733, upload-time = "2026-07-22T11:51:16.027Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e9/10/2392fc3a98948652ef5fd1e7275c04f861dd13f74b78a2b4309f4ee4d090/greenlet-3.5.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f00f910f0e7b35416c63b23ad78b769aeccfc1775f712b43c4ee525624a2eef7", size = 1637327, upload-time = "2026-07-22T12:25:07.879Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/55/c6/e7237a3dfa1f205ed0d9ea1e46d70bd2811b32d516266399fb59d28ab90a/greenlet-3.5.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:91c26423753b92caf41ab3f98fd547d7374d4d9fc2d85be041886c1579d9255e", size = 1697493, upload-time = "2026-07-22T11:51:19.214Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/55/e3/4ba8154ba2a3d43729e499f72471b4b5c993f3826d3e24da81d5f06d6572/greenlet-3.5.4-cp314-cp314t-win_amd64.whl", hash = "sha256:ee032b91fd8ec29ec6c4cea2b8c561b178435134bd0752c7334b94e9c736c132", size = 251637, upload-time = "2026-07-22T11:40:37.44Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/90/03/e3f96dfc100261a29545ddc8270cafe58f9195b6651466910e820910de77/greenlet-3.5.4-cp315-cp315-macosx_11_0_universal2.whl", hash = "sha256:178111881dd7a6c946471fda85485ec796e1043c2b939f694b096e2ecf986809", size = 296076, upload-time = "2026-07-22T11:39:38.364Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a4/3d/da52d208e5c977bce8667e784729e584e38b5785f4c1ec0f4c836e9a1c42/greenlet-3.5.4-cp315-cp315-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d92df08dd65fede97fc37aad36c2e9dcda3b31c467f8e0c2c096456cb818e927", size = 666870, upload-time = "2026-07-22T12:26:46.691Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/2d/8a/7e6dee25cb8a8cf9b362c8e597cc269593378bd916f16c736c059a52e85a/greenlet-3.5.4-cp315-cp315-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:99e8f8c4ebc4fd80aa26c1280ae9ad43a0976e786349703a181cf0bae60413e5", size = 677678, upload-time = "2026-07-22T12:29:07.508Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/6c/21/5a38699fa45de749e3857d93b8f07e4c20489e77c2d35d915a2e1c456606/greenlet-3.5.4-cp315-cp315-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:394de08dad5ffcb1f50c2159d93e398d9d2da3ed437645eaa54771fa720db9f0", size = 676067, upload-time = "2026-07-22T11:51:18.163Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/5e/ba/863116ab8ff1ca7a729e327800268939d182db47aa433db70e216e7d9194/greenlet-3.5.4-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:c883d61f2282d72c767a14936641b3efcbde9d82f1080712aaea0b1d3126cb88", size = 1633489, upload-time = "2026-07-22T12:25:09.605Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/fa/06/7466ced82818d6132462d7f26b3f83c66ea15d2b193a6c0088d558ed7d95/greenlet-3.5.4-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:2a924f15d17957e252a810acefcb5942f5ca712298e8b6fcaed9a307d357522c", size = 1696584, upload-time = "2026-07-22T11:51:21.304Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f9/4d/55b638489260065de9ffce606c8b5d04507bef705de4b212a0c3d6a1a0df/greenlet-3.5.4-cp315-cp315-win_amd64.whl", hash = "sha256:ed17e5f3420360d5b459de8462efb52060399a5326a613d4cde31cef63ef95da", size = 248297, upload-time = "2026-07-22T11:42:04.055Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/bb/08/9dd4ae635da93d41dc268bc34bd62a9d711ed8b8825c5d22ac910c7d6e6d/greenlet-3.5.4-cp315-cp315-win_arm64.whl", hash = "sha256:f908898d6fa484ce4b6f447ce70ea99b52c503fee419e53cf74d60a16bc9e667", size = 247423, upload-time = "2026-07-22T11:44:00.764Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/19/66/7c87ed9cdbf1d49c2c6cd1c7b9dd4d16c33b24235ca03972293a1876b30c/greenlet-3.5.4-cp315-cp315t-macosx_11_0_universal2.whl", hash = "sha256:1833637f17d5e7472548a48575c394fe39f1b1890d676d162d86593610f44d8c", size = 306487, upload-time = "2026-07-22T11:41:25.118Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/5b/05/0a4201e7c0054866eefc05da234f236dd4c950d0fbf9ca0517141f01b269/greenlet-3.5.4-cp315-cp315t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:12cda9122e03341f1cb6b8207a19d7a9d375e52f1b4e9243918375f40fd7b4b9", size = 676479, upload-time = "2026-07-22T12:26:48.129Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/3d/c0/4b6b8c5a3aec70f0649cd89662d120fdd6421e2bdc8e3b15c3ab5ec568d8/greenlet-3.5.4-cp315-cp315t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d83ae0e32d14957ab7170785a20f582635c8474deab1bfbb552b17e769a6ce25", size = 684321, upload-time = "2026-07-22T12:29:08.925Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/24/c9/b49c31c9a972eee91e260445770e922244a7efc542697f51a012ec046d0f/greenlet-3.5.4-cp315-cp315t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9f1467de1bb767f75db0aa34c195e3a496d8d1278c796e70c24ce205d3e99cde", size = 681293, upload-time = "2026-07-22T11:51:20.43Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/95/6f/7f2d4653770500eee667866016d42d7a68e3d3462f80df6b8e3fcd48a0eb/greenlet-3.5.4-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:0fa53040b78b578120eecdc0265e3f1051487cc425d11a2b7c761daadf4feaa8", size = 1642474, upload-time = "2026-07-22T12:25:10.819Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/5a/d8/8cba31036a4caae448087ba5d150660ab03b4a0f54d9150f6495a3be7262/greenlet-3.5.4-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:60e0bc961d367df506660e9ac0177a76bc6d81305300704b0977d1634f76efe2", size = 1701012, upload-time = "2026-07-22T11:51:23.17Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e3/cd/3f77a4cce3bae631b08eb52f53a82a976669600337e21dfdba811cb50267/greenlet-3.5.4-cp315-cp315t-win_amd64.whl", hash = "sha256:f680e549edb3eaf21eea4e7fe101e15ec180c74b7879ab46adc080f22d4015d2", size = 251977, upload-time = "2026-07-22T11:41:38.125Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/93/e8/65e8707d00fe2a49bf12f609a9b2b39ba6dd23c2810eacad877c4fc94bfe/greenlet-3.5.4-cp315-cp315t-win_arm64.whl", hash = "sha256:08fc36de8442d5c3e95b044550dbea9bf144d31ec0cc58e36fb241cb6ef6a994", size = 250538, upload-time = "2026-07-22T11:40:17.985Z" }, + { url = "https://files.pythonhosted.org/packages/f3/04/81bd731d6d1e3a469d9a4c36f5eb069bcf0cbb2d5d342c9fec22245b91fc/greenlet-3.5.4-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:3d66250e8b09f182ede05490998c818b5961f7a3640332d44c4927caec7bbfe4", size = 295909, upload-time = "2026-07-22T11:38:09.261Z" }, + { url = "https://files.pythonhosted.org/packages/cc/dd/f5f22903a6ae70f5ea328ed0beaec92ad903f0e3b7d2845133b354abc4b8/greenlet-3.5.4-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c90e930c9c192e5b3ee9fb8bcd920ea3926155e2e3ded39fc697323addecee17", size = 612011, upload-time = "2026-07-22T12:26:40.69Z" }, + { url = "https://files.pythonhosted.org/packages/8e/10/92a4a88d12b915d74ea5b6d288e4afefda4771647caa34442c156f7a454f/greenlet-3.5.4-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:791fdfeeb9c6e0c7b10fa151bf110d2a6974866f13dcb5b1c7efae698245893a", size = 624299, upload-time = "2026-07-22T12:29:02.089Z" }, + { url = "https://files.pythonhosted.org/packages/50/6d/0b14bb9db2989f32cd9fe7f76afedea01ee8bee3f87c07e69f24adfe7e63/greenlet-3.5.4-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f88193799d43dbf8c8a806d6405c9c52fe2af40bf75072a606357b33cc336c7f", size = 621541, upload-time = "2026-07-22T11:51:09.464Z" }, + { url = "https://files.pythonhosted.org/packages/48/3d/25e9a2d9eb6b2e8b7ca4e80a3a26cb887cce6c8e0a87c921164f11bc5574/greenlet-3.5.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b7a5f095767c4493afcd06067f2bb3b8716e3f3f9e92b99c88e7e99f885b3d4d", size = 1581444, upload-time = "2026-07-22T12:25:03.818Z" }, + { url = "https://files.pythonhosted.org/packages/b9/96/4c9bf2e2c408dcc0556edce69efa9f802e82223573c53240136a086821f1/greenlet-3.5.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:42afdc1ab5f66da8c586c32af9224a74a706b4f0ea0dc3a4188a0860a09c65c9", size = 1645842, upload-time = "2026-07-22T11:51:12.295Z" }, + { url = "https://files.pythonhosted.org/packages/b5/41/303ecb26a3a56122c0f4d4073ee078881847bd6b6f463ae0ec57ec20223b/greenlet-3.5.4-cp312-cp312-win_amd64.whl", hash = "sha256:60149df8f462d1b230038e6590c23c3b4768bb5d6c022b3b6e82532b34b0b8a3", size = 247169, upload-time = "2026-07-22T11:38:19.893Z" }, + { url = "https://files.pythonhosted.org/packages/a4/e3/ef56864b4c35fcb3eb3b41b869f6cc46f4cd3f5e2c68e74acde8ac433951/greenlet-3.5.4-cp312-cp312-win_arm64.whl", hash = "sha256:77d6ce04fed0d9aeed42e0f37923cc43eba9b027bdd9c34546bb4ccd143d0fe0", size = 245565, upload-time = "2026-07-22T11:38:27.061Z" }, + { url = "https://files.pythonhosted.org/packages/c0/9a/e51225dcd58713f16ccbdcc501a8da21098ea14515b7870f1f94459e5ff5/greenlet-3.5.4-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:24e61b88cb7e1b1d794b32a10cc346ac779681d6d74ff137a3e0a444d2bf1f02", size = 294831, upload-time = "2026-07-22T11:38:53.389Z" }, + { url = "https://files.pythonhosted.org/packages/9f/ea/de50a50fadf979713ab18b46f22ad5ff5f2dcfc637a3ebdecf669801e1a5/greenlet-3.5.4-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:870d730fec833f5a06906a32596cc099b9161594642a92a520b7a88911c95356", size = 614619, upload-time = "2026-07-22T12:26:42.282Z" }, + { url = "https://files.pythonhosted.org/packages/db/c7/2aae27fea41205b8650294c301f042a2a4bb6155eea48c995b890a92f2c1/greenlet-3.5.4-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ec5ff0d1878df6af3bf9b638a5a92a7d5693291de77c91bff10fa48519c604ef", size = 627021, upload-time = "2026-07-22T12:29:03.445Z" }, + { url = "https://files.pythonhosted.org/packages/eb/56/79fd826f9ccaae0b84e1b4ef68dabba5e105bb044ffcd448a0b782fcba9a/greenlet-3.5.4-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d84d993f6e575c950d91a23c1345d18fe1a4310d447bf630849d7809196b52f0", size = 624002, upload-time = "2026-07-22T11:51:11.391Z" }, + { url = "https://files.pythonhosted.org/packages/0a/1a/27319f97e731298513dcba1a2e91b63e9d8811d9de22130f960b129b1bf1/greenlet-3.5.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:58023945f421093de5e6fa108c0985a8659d43f49e0216da25099369a121bcbd", size = 1581533, upload-time = "2026-07-22T12:25:05.322Z" }, + { url = "https://files.pythonhosted.org/packages/b1/6d/24240bf562e9786dd2799ee0a4a4dadb4ded22510f41b20245099159ac8c/greenlet-3.5.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bae2728e1897aa8df8cb1af38cd48b3a743aefe29372de7b8b7a9f532501e69f", size = 1645781, upload-time = "2026-07-22T11:51:14.805Z" }, + { url = "https://files.pythonhosted.org/packages/c1/5a/442ab1a9ef7ca6bf7210e5397a95972206a91a31033a03c8900866a10039/greenlet-3.5.4-cp313-cp313-win_amd64.whl", hash = "sha256:ca5726c0b08ca35ae873557266a78b2c3f3b2b7d7401aa5ff886c2045dd0111c", size = 247133, upload-time = "2026-07-22T11:39:20.661Z" }, + { url = "https://files.pythonhosted.org/packages/3e/e6/9160210222386b1a378ff94db846b9508ca24a121cf684991561fdb69280/greenlet-3.5.4-cp313-cp313-win_arm64.whl", hash = "sha256:7c1303791d603080cac6fc3b34df51c3b75b723739c282c8029e48a0d241672f", size = 245500, upload-time = "2026-07-22T11:40:22.185Z" }, + { url = "https://files.pythonhosted.org/packages/a5/a7/6ab1d4f9cd548d15ab90da29947f2076100130bb179b0bde59f795a459e3/greenlet-3.5.4-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:7e8afa5eac028f8140ceafe5ceec66e6aa127ddcb21452d2a564dcd2900b5f22", size = 295410, upload-time = "2026-07-22T11:40:35.747Z" }, + { url = "https://files.pythonhosted.org/packages/cd/7a/422f63b4715cbc0b24385305407adf38b48f6bb68b3e6b04090e994d0f5a/greenlet-3.5.4-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:73b37afe369021423ea53dd3123e04bffa7e93ac64429b9f50835b2e4fcae7cf", size = 661286, upload-time = "2026-07-22T12:26:43.8Z" }, + { url = "https://files.pythonhosted.org/packages/d0/31/5a1cac663bf5582190c5a714ef81364f03cde232227f39748f8ae4c11da5/greenlet-3.5.4-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3ef964f56dfcb6f9bbef2a190d9126795eac408716aeae47b5e7c73c32aafca9", size = 673517, upload-time = "2026-07-22T12:29:04.815Z" }, + { url = "https://files.pythonhosted.org/packages/15/4a/2a82a1e3f8aaca020853ac8d12211280ca2b231aa08ea39f636f1060c319/greenlet-3.5.4-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c53ff01a5c53a40f2c16820ebc56d7c61a77f5fbe009dadd96292d5682f80f8", size = 670917, upload-time = "2026-07-22T11:51:13.589Z" }, + { url = "https://files.pythonhosted.org/packages/68/b0/e379a152b17bfdfa95795af4049e37c0fd1b4d81f020d426db104ed07c77/greenlet-3.5.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ecca4d80d55a01ad6b23b33262662956149fbb7b2c6be2910f1705921958cbf3", size = 1628478, upload-time = "2026-07-22T12:25:06.678Z" }, + { url = "https://files.pythonhosted.org/packages/5e/43/bffdfa64f7317f954c5c1230b5dd5922676ce198689a68c1ac1ed4b1b1a5/greenlet-3.5.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2ffbc533e0eaf8e80d8471411646ab88fe58f641d508c0b02b24494479f4d9ec", size = 1692021, upload-time = "2026-07-22T11:51:17.008Z" }, + { url = "https://files.pythonhosted.org/packages/d0/11/f799f9637e2c6e9b0b716015e339040598b058cf7654dfc0d67468b177ed/greenlet-3.5.4-cp314-cp314-win_amd64.whl", hash = "sha256:305f69e6c4523d7f6979ed001cff4e5853c063e5da04880296603aa0227e544c", size = 248031, upload-time = "2026-07-22T11:40:11.007Z" }, + { url = "https://files.pythonhosted.org/packages/05/75/625bcdd74d5e6b2dca1ecba3c3ac77bcf8a026c21a649a46cef23e421f97/greenlet-3.5.4-cp314-cp314-win_arm64.whl", hash = "sha256:f260930bbbbcf9caee661211235a5111c86dfe5832fdf6ae4570da1e0995320f", size = 246892, upload-time = "2026-07-22T11:40:27.357Z" }, + { url = "https://files.pythonhosted.org/packages/ec/69/35c62ed49c320cb4d98e14698ccca5467d3bfe683984172be9cb564d9ce3/greenlet-3.5.4-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:41ddab54e4b238f4a6c323f39b4e59e176affd5a94d461a9fb7583dac74240a3", size = 305571, upload-time = "2026-07-22T11:40:31.659Z" }, + { url = "https://files.pythonhosted.org/packages/5b/6c/64d60216b3640dcb0b62d913dd9e0d80030c09115bb2e4ba70c95d10ca45/greenlet-3.5.4-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b3dabe3e2809013052c68bdf0b7fa5f5f2859c43a80803131ad61af9cabd7867", size = 672568, upload-time = "2026-07-22T12:26:45.298Z" }, + { url = "https://files.pythonhosted.org/packages/5c/de/ba3ab0a96292e53039530333b0d2ae18d9e508f3a325cd7bf15f8172944c/greenlet-3.5.4-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:27d3f00718634d4520a3a150154ac5da36f257869d41321953375b90bfbbc72c", size = 680076, upload-time = "2026-07-22T12:29:06.125Z" }, + { url = "https://files.pythonhosted.org/packages/a5/be/aeada79083c6f1c15f45d77a332f9c441af263ee298e3eb17522cd337d22/greenlet-3.5.4-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cbd60b5763c6543c1827e48faaf14ea9bfbad245f52b1a4d76a2a2d8884c6c66", size = 676733, upload-time = "2026-07-22T11:51:16.027Z" }, + { url = "https://files.pythonhosted.org/packages/e9/10/2392fc3a98948652ef5fd1e7275c04f861dd13f74b78a2b4309f4ee4d090/greenlet-3.5.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f00f910f0e7b35416c63b23ad78b769aeccfc1775f712b43c4ee525624a2eef7", size = 1637327, upload-time = "2026-07-22T12:25:07.879Z" }, + { url = "https://files.pythonhosted.org/packages/55/c6/e7237a3dfa1f205ed0d9ea1e46d70bd2811b32d516266399fb59d28ab90a/greenlet-3.5.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:91c26423753b92caf41ab3f98fd547d7374d4d9fc2d85be041886c1579d9255e", size = 1697493, upload-time = "2026-07-22T11:51:19.214Z" }, + { url = "https://files.pythonhosted.org/packages/55/e3/4ba8154ba2a3d43729e499f72471b4b5c993f3826d3e24da81d5f06d6572/greenlet-3.5.4-cp314-cp314t-win_amd64.whl", hash = "sha256:ee032b91fd8ec29ec6c4cea2b8c561b178435134bd0752c7334b94e9c736c132", size = 251637, upload-time = "2026-07-22T11:40:37.44Z" }, + { url = "https://files.pythonhosted.org/packages/90/03/e3f96dfc100261a29545ddc8270cafe58f9195b6651466910e820910de77/greenlet-3.5.4-cp315-cp315-macosx_11_0_universal2.whl", hash = "sha256:178111881dd7a6c946471fda85485ec796e1043c2b939f694b096e2ecf986809", size = 296076, upload-time = "2026-07-22T11:39:38.364Z" }, + { url = "https://files.pythonhosted.org/packages/a4/3d/da52d208e5c977bce8667e784729e584e38b5785f4c1ec0f4c836e9a1c42/greenlet-3.5.4-cp315-cp315-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d92df08dd65fede97fc37aad36c2e9dcda3b31c467f8e0c2c096456cb818e927", size = 666870, upload-time = "2026-07-22T12:26:46.691Z" }, + { url = "https://files.pythonhosted.org/packages/2d/8a/7e6dee25cb8a8cf9b362c8e597cc269593378bd916f16c736c059a52e85a/greenlet-3.5.4-cp315-cp315-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:99e8f8c4ebc4fd80aa26c1280ae9ad43a0976e786349703a181cf0bae60413e5", size = 677678, upload-time = "2026-07-22T12:29:07.508Z" }, + { url = "https://files.pythonhosted.org/packages/6c/21/5a38699fa45de749e3857d93b8f07e4c20489e77c2d35d915a2e1c456606/greenlet-3.5.4-cp315-cp315-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:394de08dad5ffcb1f50c2159d93e398d9d2da3ed437645eaa54771fa720db9f0", size = 676067, upload-time = "2026-07-22T11:51:18.163Z" }, + { url = "https://files.pythonhosted.org/packages/5e/ba/863116ab8ff1ca7a729e327800268939d182db47aa433db70e216e7d9194/greenlet-3.5.4-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:c883d61f2282d72c767a14936641b3efcbde9d82f1080712aaea0b1d3126cb88", size = 1633489, upload-time = "2026-07-22T12:25:09.605Z" }, + { url = "https://files.pythonhosted.org/packages/fa/06/7466ced82818d6132462d7f26b3f83c66ea15d2b193a6c0088d558ed7d95/greenlet-3.5.4-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:2a924f15d17957e252a810acefcb5942f5ca712298e8b6fcaed9a307d357522c", size = 1696584, upload-time = "2026-07-22T11:51:21.304Z" }, + { url = "https://files.pythonhosted.org/packages/f9/4d/55b638489260065de9ffce606c8b5d04507bef705de4b212a0c3d6a1a0df/greenlet-3.5.4-cp315-cp315-win_amd64.whl", hash = "sha256:ed17e5f3420360d5b459de8462efb52060399a5326a613d4cde31cef63ef95da", size = 248297, upload-time = "2026-07-22T11:42:04.055Z" }, + { url = "https://files.pythonhosted.org/packages/bb/08/9dd4ae635da93d41dc268bc34bd62a9d711ed8b8825c5d22ac910c7d6e6d/greenlet-3.5.4-cp315-cp315-win_arm64.whl", hash = "sha256:f908898d6fa484ce4b6f447ce70ea99b52c503fee419e53cf74d60a16bc9e667", size = 247423, upload-time = "2026-07-22T11:44:00.764Z" }, + { url = "https://files.pythonhosted.org/packages/19/66/7c87ed9cdbf1d49c2c6cd1c7b9dd4d16c33b24235ca03972293a1876b30c/greenlet-3.5.4-cp315-cp315t-macosx_11_0_universal2.whl", hash = "sha256:1833637f17d5e7472548a48575c394fe39f1b1890d676d162d86593610f44d8c", size = 306487, upload-time = "2026-07-22T11:41:25.118Z" }, + { url = "https://files.pythonhosted.org/packages/5b/05/0a4201e7c0054866eefc05da234f236dd4c950d0fbf9ca0517141f01b269/greenlet-3.5.4-cp315-cp315t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:12cda9122e03341f1cb6b8207a19d7a9d375e52f1b4e9243918375f40fd7b4b9", size = 676479, upload-time = "2026-07-22T12:26:48.129Z" }, + { url = "https://files.pythonhosted.org/packages/3d/c0/4b6b8c5a3aec70f0649cd89662d120fdd6421e2bdc8e3b15c3ab5ec568d8/greenlet-3.5.4-cp315-cp315t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d83ae0e32d14957ab7170785a20f582635c8474deab1bfbb552b17e769a6ce25", size = 684321, upload-time = "2026-07-22T12:29:08.925Z" }, + { url = "https://files.pythonhosted.org/packages/24/c9/b49c31c9a972eee91e260445770e922244a7efc542697f51a012ec046d0f/greenlet-3.5.4-cp315-cp315t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9f1467de1bb767f75db0aa34c195e3a496d8d1278c796e70c24ce205d3e99cde", size = 681293, upload-time = "2026-07-22T11:51:20.43Z" }, + { url = "https://files.pythonhosted.org/packages/95/6f/7f2d4653770500eee667866016d42d7a68e3d3462f80df6b8e3fcd48a0eb/greenlet-3.5.4-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:0fa53040b78b578120eecdc0265e3f1051487cc425d11a2b7c761daadf4feaa8", size = 1642474, upload-time = "2026-07-22T12:25:10.819Z" }, + { url = "https://files.pythonhosted.org/packages/5a/d8/8cba31036a4caae448087ba5d150660ab03b4a0f54d9150f6495a3be7262/greenlet-3.5.4-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:60e0bc961d367df506660e9ac0177a76bc6d81305300704b0977d1634f76efe2", size = 1701012, upload-time = "2026-07-22T11:51:23.17Z" }, + { url = "https://files.pythonhosted.org/packages/e3/cd/3f77a4cce3bae631b08eb52f53a82a976669600337e21dfdba811cb50267/greenlet-3.5.4-cp315-cp315t-win_amd64.whl", hash = "sha256:f680e549edb3eaf21eea4e7fe101e15ec180c74b7879ab46adc080f22d4015d2", size = 251977, upload-time = "2026-07-22T11:41:38.125Z" }, + { url = "https://files.pythonhosted.org/packages/93/e8/65e8707d00fe2a49bf12f609a9b2b39ba6dd23c2810eacad877c4fc94bfe/greenlet-3.5.4-cp315-cp315t-win_arm64.whl", hash = "sha256:08fc36de8442d5c3e95b044550dbea9bf144d31ec0cc58e36fb241cb6ef6a994", size = 250538, upload-time = "2026-07-22T11:40:17.985Z" }, ] [[package]] name = "h11" version = "0.16.0" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" } -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, ] [[package]] name = "httpcore" version = "1.0.9" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" } +source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, { name = "h11" }, ] -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httptools" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/e5/d471fcb0e14523fe1c3f4ba58ca52480e7bd70ad7109a3846bc75892f7fb/httptools-0.8.0.tar.gz", hash = "sha256:6b2a32f18d97e16e90827d7a819ffa8dbd8cc245fc4e1fa9d1095b54ef4bd999", size = 271342, upload-time = "2026-05-25T22:17:48.841Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/88/1d21a36da8f5cb0fa49eafd4b169eba5608d57e75bbcf61845cbc6243216/httptools-0.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:880490234c10f70a9830743097e8958d6e4b9f5a0ffc24515023afeef984054d", size = 208247, upload-time = "2026-05-25T22:17:07.843Z" }, + { url = "https://files.pythonhosted.org/packages/a5/42/cc4feea2945cb3051038f090c9b36bd5b8a9d7f5a894a506a8983e33fd1c/httptools-0.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5931891fb7b441b8a3853cf1b85c82c903defce084dd5f6771ca46e31bf862c5", size = 113064, upload-time = "2026-05-25T22:17:09.136Z" }, + { url = "https://files.pythonhosted.org/packages/e3/a6/febbb8b8db0f58b38e44ad6cb946e6a255ae49b55f2e8543408fb7501ccd/httptools-0.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b15fc622b0f869d19207c4089a501d9bcc63ca5e071ffdd2f03f922df882dcb2", size = 523851, upload-time = "2026-05-25T22:17:10.106Z" }, + { url = "https://files.pythonhosted.org/packages/b7/e4/f90a0df0b83beff265b7e3b65f2a4cefd95792d4be0ac3e16049f2acd3c2/httptools-0.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:425f83884fd6343828d8c565f046cb72b6d19063f6924093e11bcd8e1548cd09", size = 518842, upload-time = "2026-05-25T22:17:11.218Z" }, + { url = "https://files.pythonhosted.org/packages/9e/2d/0c9ac76dd2c893841fbf6498d6acec4f2442e1b7067f6e3e316a80e494e8/httptools-0.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ef7c3c97f4311c7be57e2986629df89d49cb434dbff78eafcd48c2bff986b15a", size = 501238, upload-time = "2026-05-25T22:17:12.728Z" }, + { url = "https://files.pythonhosted.org/packages/ca/42/906adc91ae3a5fa9c59c0a2f21c139725bd7e5b41ae6acd485cd14123ebf/httptools-0.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a1afd7c9fbff0d9f5d489c4ce2768bd09c84a46ddefc7161e6aa82ae35c85745", size = 509567, upload-time = "2026-05-25T22:17:13.842Z" }, + { url = "https://files.pythonhosted.org/packages/05/0b/4240efeb672751ee5b9b380cb0e3fdc050bc05f68adc7a8aefc4fcd9a69a/httptools-0.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:cd96f29b4bab1d42fa6e3d008711c75e0f79e94e06827330160e3a304227f150", size = 90918, upload-time = "2026-05-25T22:17:15.155Z" }, + { url = "https://files.pythonhosted.org/packages/5e/e5/8cfcabc5546e8022f168be28bcdaa128a240a0befdd03b59d558b4f18bd6/httptools-0.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:614ceea8ea606848bece2338ac03b3ce5324bcb4be8dc7d377ed708012fa4db8", size = 205148, upload-time = "2026-05-25T22:17:16.333Z" }, + { url = "https://files.pythonhosted.org/packages/2a/0e/0fb14848c19a686c8062ff9067c1a48793e3224b47bc5b201535b6036fce/httptools-0.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2d689918c15a013c65ef52d9fd495d766893ab831a2c8d89f2ac5940a5df847c", size = 111368, upload-time = "2026-05-25T22:17:17.586Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1b/46f1cecf06b9bbde8e4b8c88034ac7908989e5ff7a3a388ef38392949c1f/httptools-0.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:eb3028cca2fc0a6d720e52ef61d8ebb62fcbfeb1de56874546d858d3f25a26b7", size = 486447, upload-time = "2026-05-25T22:17:18.564Z" }, + { url = "https://files.pythonhosted.org/packages/77/00/258bfc0837221f81d9725c45f9b948a6a6b2994a147a4fb66e85100c668f/httptools-0.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:88bdd940f2b5d487b4d032c6afa5489a7dc4694410d43de3c38c4fb3af0dc45d", size = 482448, upload-time = "2026-05-25T22:17:19.912Z" }, + { url = "https://files.pythonhosted.org/packages/04/ab/d1cef3b5523f4d272a70f42a776c3169a2dddfe3a54de4b2ce4a36341528/httptools-0.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6a43c9dd399758ccc0531acb0a3c4a6c299ee893ee9400e9c893b7bdcfae0681", size = 464460, upload-time = "2026-05-25T22:17:20.882Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/5d1d072442277bb2b3434e0e60690b8e8c23840ef7de8b6ea54040a536d3/httptools-0.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0770728beb05094c809b98e814edff5fef69d26ad7d21185f2f6d5884a0ba683", size = 471312, upload-time = "2026-05-25T22:17:22.085Z" }, + { url = "https://files.pythonhosted.org/packages/0d/66/b96623b27e51a68199ef4efdda0613cced9233fe3062ac74e50749c5ad37/httptools-0.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:7685df791fad561384bfb139e77fde27a1ffd93134e016f95a0db424ffbf77b1", size = 90117, upload-time = "2026-05-25T22:17:23.074Z" }, + { url = "https://files.pythonhosted.org/packages/1a/12/fa3fbf5f9517b273edea2dc982aa82a8c634091e67c590792b729017bc6f/httptools-0.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:de242a49b5d18e0a8776e654e9f6bf6d89f3875a5c35b425a0e7ce940feb3fd6", size = 206183, upload-time = "2026-05-25T22:17:24.004Z" }, + { url = "https://files.pythonhosted.org/packages/30/fc/5e7c4cb443370f2090a3aba0453a07384d29ff66b7435bb90e77e1037599/httptools-0.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:159e9ab5f701ccd42e555a12f1ad8ff69702910fc1c996cf2bb66e5fcb7a231b", size = 112079, upload-time = "2026-05-25T22:17:25.216Z" }, + { url = "https://files.pythonhosted.org/packages/ba/53/771bd891eb0f236f32145d6a1775777ec85745f3cc983a1f23d1a3b8ddfe/httptools-0.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c4a9f1707e4823d54dfec6c33fa3697d302aed536ed352a7ebb5a061ddb869d0", size = 481596, upload-time = "2026-05-25T22:17:26.186Z" }, + { url = "https://files.pythonhosted.org/packages/62/42/94e15bc68ce3d423243c45d7f1b0c7561f13844f97dc52ae23182fb65628/httptools-0.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d76ad7b951387e3632c8716a9bb03ac5b45c5f16119aa409db0459520887944e", size = 480865, upload-time = "2026-05-25T22:17:27.542Z" }, + { url = "https://files.pythonhosted.org/packages/1c/7c/fe2980fc03723272e30f135b62360b075f513dfe7cc73aef36c7f04012bd/httptools-0.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a3b7387147361c3fd47a0bde763c5c91b5b4cd4dc9989b8ece84ff436c99843b", size = 463189, upload-time = "2026-05-25T22:17:28.546Z" }, + { url = "https://files.pythonhosted.org/packages/15/1b/47fc5fff68acd1bfa20b4734059c9a06cadb88119dcd5258b5b0d21d91c8/httptools-0.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f256d6ce930c52ca1cb2a960b7da03548c454e7d28b06059ad41bfe789036ce0", size = 466610, upload-time = "2026-05-25T22:17:29.816Z" }, + { url = "https://files.pythonhosted.org/packages/60/bd/07b13c93ffd9bec9546e0d43f8e19378dd696dbd278511406bc07371ef1f/httptools-0.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:19d1ee275bb59ba2643ba9a3a1e51cc0c788caf2b8df506368e03f56fdd08527", size = 92705, upload-time = "2026-05-25T22:17:31.133Z" }, + { url = "https://files.pythonhosted.org/packages/fd/c4/121648f68ce066d7bd762d6b6d97e620847642d38d54f3d90ff11d947629/httptools-0.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:de1ed58a974e75d56560acc7e7fed01a454994429456f65209789992e41f2568", size = 215023, upload-time = "2026-05-25T22:17:32.401Z" }, + { url = "https://files.pythonhosted.org/packages/b9/b0/312a062ae741ae3e8baa8c8bf20be81b2e67337b259ab4349bebc7b6142e/httptools-0.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e93c227b595c6926c1acee96891dd9da4be338cfbe82e5cd3bb9d8dd7dc4ac0b", size = 117405, upload-time = "2026-05-25T22:17:33.742Z" }, + { url = "https://files.pythonhosted.org/packages/fc/37/fccd705f795386bb05bf413012fecff2a33e5aa8c2f069096de3e9fd8702/httptools-0.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2a021c3a8e65cc125390d72f59b968afca3bdcaff25bd67965e0a055a14946ca", size = 558497, upload-time = "2026-05-25T22:17:34.732Z" }, + { url = "https://files.pythonhosted.org/packages/bd/39/f172e8003576de35f5ba77ff417cf0e34429d35dc014deef15afa337a72c/httptools-0.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48774d39cbb70e2b1f71f88852a3087ae1d3a1eb80482bb48c13067ab080c14f", size = 571585, upload-time = "2026-05-25T22:17:35.813Z" }, + { url = "https://files.pythonhosted.org/packages/3e/b9/f5564760af99f3dbbf3f9104dc00e5da27e96cf433c6bdcf77617f70bf3f/httptools-0.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:88eead8ec8680a9f146c655bc88445a325bd7921cfd8194c7337e9467282427d", size = 543297, upload-time = "2026-05-25T22:17:37.08Z" }, + { url = "https://files.pythonhosted.org/packages/99/67/8d9f2c313618e161b82f3873188e7196126da1d6e29688df40eb3997c77a/httptools-0.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2c032fa028f46871ec7e1fc59fc15e8023eab3e6bbe6ece786a1611719a5d081", size = 539535, upload-time = "2026-05-25T22:17:38.032Z" }, + { url = "https://files.pythonhosted.org/packages/48/63/b906c01e53f50d432c0defe43ce52764a111dc1bdd028bafbeb54dcfd008/httptools-0.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:384c17174464c8e873398b7af24f0b1f44d992c820328413951a625323155d77", size = 108209, upload-time = "2026-05-25T22:17:39.473Z" }, ] [[package]] name = "httpx" version = "0.28.1" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" } +source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, { name = "certifi" }, { name = "httpcore" }, { name = "idna" }, ] -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, ] [[package]] name = "idna" version = "3.18" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" } -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" } +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" } wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" }, + { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" }, ] [[package]] name = "ipykernel" -version = "7.3.0" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" } +version = "6.29.5" +source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "appnope", marker = "sys_platform == 'darwin'" }, { name = "comm" }, @@ -641,22 +571,22 @@ dependencies = [ { name = "jupyter-client" }, { name = "jupyter-core" }, { name = "matplotlib-inline" }, - { name = "nest-asyncio2" }, + { name = "nest-asyncio" }, { name = "packaging" }, { name = "psutil" }, { name = "pyzmq" }, { name = "tornado" }, { name = "traitlets" }, ] -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/3d/c4/e4a38f579de4225a561305666f7541cdabb30075def2aa1ac17bd73c1fb5/ipykernel-7.3.0.tar.gz", hash = "sha256:9acaaaf97d16355166e4085afe9d225bfbdf2b7ef520f9df3be8f2b248275e09", size = 184899, upload-time = "2026-06-10T08:41:25.481Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e9/5c/67594cb0c7055dc50814b21731c22a601101ea3b1b50a9a1b090e11f5d0f/ipykernel-6.29.5.tar.gz", hash = "sha256:f093a22c4a40f8828f8e330a9c297cb93dcab13bd9678ded6de8e5cf81c56215", size = 163367, upload-time = "2024-07-01T14:07:22.543Z" } wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/3d/02/77b271f5dc58bfbc0b577c877b2365d1ffea2afe66a80c13f2312820348c/ipykernel-7.3.0-py3-none-any.whl", hash = "sha256:897eb64da762549ef610698fca5e9675195ec6ac8ec7f19d81ce1ca20c876057", size = 120583, upload-time = "2026-06-10T08:41:23.648Z" }, + { url = "https://files.pythonhosted.org/packages/94/5c/368ae6c01c7628438358e6d337c19b05425727fbb221d2a3c4303c372f42/ipykernel-6.29.5-py3-none-any.whl", hash = "sha256:afdb66ba5aa354b09b91379bac28ae4afebbb30e8b39510c9690afb7a10421b5", size = 117173, upload-time = "2024-07-01T14:07:19.603Z" }, ] [[package]] name = "ipython" version = "9.15.0" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" } +source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, { name = "decorator" }, @@ -670,143 +600,75 @@ dependencies = [ { name = "stack-data" }, { name = "traitlets" }, ] -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/53/59/165d3b4d75cc34add3122c4417ecb229085140ac573103c223cd01dde96f/ipython-9.15.0.tar.gz", hash = "sha256:da2819ce2aa83135257df830660b1176d986c3d2876db24df01974fa955b2756", size = 4442580, upload-time = "2026-06-26T11:03:35.913Z" } +sdist = { url = "https://files.pythonhosted.org/packages/53/59/165d3b4d75cc34add3122c4417ecb229085140ac573103c223cd01dde96f/ipython-9.15.0.tar.gz", hash = "sha256:da2819ce2aa83135257df830660b1176d986c3d2876db24df01974fa955b2756", size = 4442580, upload-time = "2026-06-26T11:03:35.913Z" } wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/40/3a/948263ca3b9d65bb2b1b0c521b3a49fad5d59ada58724bd87d2bd5ff3f36/ipython-9.15.0-py3-none-any.whl", hash = "sha256:515ad9c3cdf0c932a5a9f6245419e8aba706b7bd03c3e1d3a1c83d9351d6aa6e", size = 630895, upload-time = "2026-06-26T11:03:33.809Z" }, + { url = "https://files.pythonhosted.org/packages/40/3a/948263ca3b9d65bb2b1b0c521b3a49fad5d59ada58724bd87d2bd5ff3f36/ipython-9.15.0-py3-none-any.whl", hash = "sha256:515ad9c3cdf0c932a5a9f6245419e8aba706b7bd03c3e1d3a1c83d9351d6aa6e", size = 630895, upload-time = "2026-06-26T11:03:33.809Z" }, ] [[package]] name = "ipython-pygments-lexers" version = "1.1.1" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" } +source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pygments" }, ] -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ef/4c/5dd1d8af08107f88c7f741ead7a40854b8ac24ddf9ae850afbcf698aa552/ipython_pygments_lexers-1.1.1.tar.gz", hash = "sha256:09c0138009e56b6854f9535736f4171d855c8c08a563a0dcd8022f78355c7e81", size = 8393, upload-time = "2025-01-17T11:24:34.505Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ef/4c/5dd1d8af08107f88c7f741ead7a40854b8ac24ddf9ae850afbcf698aa552/ipython_pygments_lexers-1.1.1.tar.gz", hash = "sha256:09c0138009e56b6854f9535736f4171d855c8c08a563a0dcd8022f78355c7e81", size = 8393, upload-time = "2025-01-17T11:24:34.505Z" } wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl", hash = "sha256:a9462224a505ade19a605f71f8fa63c2048833ce50abc86768a0d81d876dc81c", size = 8074, upload-time = "2025-01-17T11:24:33.271Z" }, -] - -[[package]] -name = "isoduration" -version = "20.11.0" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" } -dependencies = [ - { name = "arrow" }, -] -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/7c/1a/3c8edc664e06e6bd06cce40c6b22da5f1429aa4224d0c590f3be21c91ead/isoduration-20.11.0.tar.gz", hash = "sha256:ac2f9015137935279eac671f94f89eb00584f940f5dc49462a0c4ee692ba1bd9", size = 11649, upload-time = "2020-11-01T11:00:00.312Z" } -wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/7b/55/e5326141505c5d5e34c5e0935d2908a74e4561eca44108fbfb9c13d2911a/isoduration-20.11.0-py3-none-any.whl", hash = "sha256:b2904c2a4228c3d44f409c8ae8e2370eb21a26f7ac2ec5446df141dde3452042", size = 11321, upload-time = "2020-11-01T10:59:58.02Z" }, + { url = "https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl", hash = "sha256:a9462224a505ade19a605f71f8fa63c2048833ce50abc86768a0d81d876dc81c", size = 8074, upload-time = "2025-01-17T11:24:33.271Z" }, ] [[package]] name = "jedi" version = "0.20.0" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" } +source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "parso" }, ] -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/46/b7/a3635f6a2d7cf5b5dd98064fc1d5fbbafcb25477bcea204a3a92145d158b/jedi-0.20.0.tar.gz", hash = "sha256:c3f4ccbd276696f4b19c54618d4fb18f9fc24b0aef02acf704b23f487daa1011", size = 3119416, upload-time = "2026-05-01T23:38:47.814Z" } +sdist = { url = "https://files.pythonhosted.org/packages/46/b7/a3635f6a2d7cf5b5dd98064fc1d5fbbafcb25477bcea204a3a92145d158b/jedi-0.20.0.tar.gz", hash = "sha256:c3f4ccbd276696f4b19c54618d4fb18f9fc24b0aef02acf704b23f487daa1011", size = 3119416, upload-time = "2026-05-01T23:38:47.814Z" } wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/9a/93/242e2eab5fe682ffcb8b0084bde703a41d51e17ee0f3a31ff0d9d813620a/jedi-0.20.0-py2.py3-none-any.whl", hash = "sha256:7bdd9c2634f56713299976f4cbd59cb3fa92165cc5e05ea811fb253480728b67", size = 4884812, upload-time = "2026-05-01T23:38:43.919Z" }, -] - -[[package]] -name = "jinja2" -version = "3.1.6" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" } -dependencies = [ - { name = "markupsafe" }, -] -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } -wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, + { url = "https://files.pythonhosted.org/packages/9a/93/242e2eab5fe682ffcb8b0084bde703a41d51e17ee0f3a31ff0d9d813620a/jedi-0.20.0-py2.py3-none-any.whl", hash = "sha256:7bdd9c2634f56713299976f4cbd59cb3fa92165cc5e05ea811fb253480728b67", size = 4884812, upload-time = "2026-05-01T23:38:43.919Z" }, ] [[package]] name = "jmespath" version = "1.1.0" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" } -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d3/59/322338183ecda247fb5d1763a6cbe46eff7222eaeebafd9fa65d4bf5cb11/jmespath-1.1.0.tar.gz", hash = "sha256:472c87d80f36026ae83c6ddd0f1d05d4e510134ed462851fd5f754c8c3cbb88d", size = 27377, upload-time = "2026-01-22T16:35:26.279Z" } +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d3/59/322338183ecda247fb5d1763a6cbe46eff7222eaeebafd9fa65d4bf5cb11/jmespath-1.1.0.tar.gz", hash = "sha256:472c87d80f36026ae83c6ddd0f1d05d4e510134ed462851fd5f754c8c3cbb88d", size = 27377, upload-time = "2026-01-22T16:35:26.279Z" } wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/14/2f/967ba146e6d58cf6a652da73885f52fc68001525b4197effc174321d70b4/jmespath-1.1.0-py3-none-any.whl", hash = "sha256:a5663118de4908c91729bea0acadca56526eb2698e83de10cd116ae0f4e97c64", size = 20419, upload-time = "2026-01-22T16:35:24.919Z" }, -] - -[[package]] -name = "json5" -version = "0.15.0" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" } -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e4/7d/05c46a96a78147ae3bf99c2f4169ce144a70220b8d6fcd56f6ec368b8ce9/json5-0.15.0.tar.gz", hash = "sha256:7424d1f1eb1d56da6e3d70643f53619862b4ce81440bdb8ecfd6f875e5ba4a71", size = 53278, upload-time = "2026-06-19T20:08:27.716Z" } -wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/eb/be/59527c99478aade6bb33a68d72e6e18dd4e6ff6eacfc7d01bdb15bc76912/json5-0.15.0-py3-none-any.whl", hash = "sha256:56636a30c0e8a4665fe2179c0212f32eae3796dea89ea6f649b9436ecdb39618", size = 36570, upload-time = "2026-06-19T20:08:26.748Z" }, -] - -[[package]] -name = "jsonpointer" -version = "3.1.1" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" } -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/18/c7/af399a2e7a67fd18d63c40c5e62d3af4e67b836a2107468b6a5ea24c4304/jsonpointer-3.1.1.tar.gz", hash = "sha256:0b801c7db33a904024f6004d526dcc53bbb8a4a0f4e32bfd10beadf60adf1900", size = 9068, upload-time = "2026-03-23T22:32:32.458Z" } -wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/9e/6a/a83720e953b1682d2d109d3c2dbb0bc9bf28cc1cbc205be4ef4be5da709d/jsonpointer-3.1.1-py3-none-any.whl", hash = "sha256:8ff8b95779d071ba472cf5bc913028df06031797532f08a7d5b602d8b2a488ca", size = 7659, upload-time = "2026-03-23T22:32:31.568Z" }, + { url = "https://files.pythonhosted.org/packages/14/2f/967ba146e6d58cf6a652da73885f52fc68001525b4197effc174321d70b4/jmespath-1.1.0-py3-none-any.whl", hash = "sha256:a5663118de4908c91729bea0acadca56526eb2698e83de10cd116ae0f4e97c64", size = 20419, upload-time = "2026-01-22T16:35:24.919Z" }, ] [[package]] name = "jsonschema" version = "4.26.0" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" } +source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, { name = "jsonschema-specifications" }, { name = "referencing" }, { name = "rpds-py" }, ] -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" } wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" }, -] - -[package.optional-dependencies] -format-nongpl = [ - { name = "fqdn" }, - { name = "idna" }, - { name = "isoduration" }, - { name = "jsonpointer" }, - { name = "rfc3339-validator" }, - { name = "rfc3986-validator" }, - { name = "rfc3987-syntax" }, - { name = "uri-template" }, - { name = "webcolors" }, + { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" }, ] [[package]] name = "jsonschema-specifications" version = "2025.9.1" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" } +source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "referencing" }, ] -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } +sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, -] - -[[package]] -name = "jupyter-builder" -version = "1.1.1" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" } -dependencies = [ - { name = "jupyter-core" }, - { name = "traitlets" }, -] -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c3/61/47f7ae054f5cd3983c10e1d65a6eb7fcd4b87ebb1056e190ef7d63ff4f19/jupyter_builder-1.1.1.tar.gz", hash = "sha256:1a13977912b08deda77fce2c803940131c27cf77a27ed64b9ffca25aa0ed7e6c", size = 971667, upload-time = "2026-07-17T13:14:47.761Z" } -wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/83/cc/f6a12de1c890ea5dd2816c5c76d5ac6d3ed52db3c37f78691328207d13b9/jupyter_builder-1.1.1-py3-none-any.whl", hash = "sha256:f9c14bc55c0488a073f62af12d468936fcf9ecb7e9dd802f6f9c33de46ad70db", size = 913264, upload-time = "2026-07-17T13:14:45.857Z" }, + { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, ] [[package]] name = "jupyter-client" version = "8.9.1" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" } +source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jupyter-core" }, { name = "python-dateutil" }, @@ -815,264 +677,109 @@ dependencies = [ { name = "traitlets" }, { name = "typing-extensions" }, ] -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/7d/dc/5512503b088997c2250b8bf18258fba9d9ce5ead641183700960d3c9d342/jupyter_client-8.9.1.tar.gz", hash = "sha256:a58f730dd9e728ba16ba1d62ebccf7ffe1ebbdbce4e95cfae941b7321ae1f4fa", size = 359256, upload-time = "2026-06-09T13:15:01.033Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/dc/5512503b088997c2250b8bf18258fba9d9ce5ead641183700960d3c9d342/jupyter_client-8.9.1.tar.gz", hash = "sha256:a58f730dd9e728ba16ba1d62ebccf7ffe1ebbdbce4e95cfae941b7321ae1f4fa", size = 359256, upload-time = "2026-06-09T13:15:01.033Z" } wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/3f/6f/56d39bf385c5c27988aebaf0c18a2a17e960575740100973511018bd904e/jupyter_client-8.9.1-py3-none-any.whl", hash = "sha256:0b7a295bc46e8751e9adae84781f726c851c1d911bd793edc4a3bde942e3da81", size = 109828, upload-time = "2026-06-09T13:14:58.835Z" }, + { url = "https://files.pythonhosted.org/packages/3f/6f/56d39bf385c5c27988aebaf0c18a2a17e960575740100973511018bd904e/jupyter_client-8.9.1-py3-none-any.whl", hash = "sha256:0b7a295bc46e8751e9adae84781f726c851c1d911bd793edc4a3bde942e3da81", size = 109828, upload-time = "2026-06-09T13:14:58.835Z" }, ] [[package]] name = "jupyter-core" version = "5.9.1" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" } +source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "platformdirs" }, { name = "traitlets" }, ] -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/02/49/9d1284d0dc65e2c757b74c6687b6d319b02f822ad039e5c512df9194d9dd/jupyter_core-5.9.1.tar.gz", hash = "sha256:4d09aaff303b9566c3ce657f580bd089ff5c91f5f89cf7d8846c3cdf465b5508", size = 89814, upload-time = "2025-10-16T19:19:18.444Z" } +sdist = { url = "https://files.pythonhosted.org/packages/02/49/9d1284d0dc65e2c757b74c6687b6d319b02f822ad039e5c512df9194d9dd/jupyter_core-5.9.1.tar.gz", hash = "sha256:4d09aaff303b9566c3ce657f580bd089ff5c91f5f89cf7d8846c3cdf465b5508", size = 89814, upload-time = "2025-10-16T19:19:18.444Z" } wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e7/e7/80988e32bf6f73919a113473a604f5a8f09094de312b9d52b79c2df7612b/jupyter_core-5.9.1-py3-none-any.whl", hash = "sha256:ebf87fdc6073d142e114c72c9e29a9d7ca03fad818c5d300ce2adc1fb0743407", size = 29032, upload-time = "2025-10-16T19:19:16.783Z" }, -] - -[[package]] -name = "jupyter-events" -version = "0.12.1" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" } -dependencies = [ - { name = "jsonschema", extra = ["format-nongpl"] }, - { name = "packaging" }, - { name = "python-json-logger" }, - { name = "pyyaml" }, - { name = "referencing" }, - { name = "rfc3339-validator" }, - { name = "rfc3986-validator" }, - { name = "traitlets" }, -] -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/18/f8/475c4241b2b75af0deaae453ed003c6c851766dbc44d332d8baf245dc931/jupyter_events-0.12.1.tar.gz", hash = "sha256:faff25f77218335752f35f23c5fe6e4a392a7bd99a5939ccb9b8fbf594636cf3", size = 62854, upload-time = "2026-04-20T23:17:50.66Z" } -wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/eb/6c/6fcde0c8f616ed360ffd3587f7db9e225a7e62b583a04494d2f069cf64ea/jupyter_events-0.12.1-py3-none-any.whl", hash = "sha256:c366585253f537a627da52fa7ca7410c5b5301fe893f511e7b077c2d93ec8bcf", size = 19512, upload-time = "2026-04-20T23:17:48.927Z" }, -] - -[[package]] -name = "jupyter-lsp" -version = "2.3.1" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" } -dependencies = [ - { name = "jupyter-server" }, -] -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/36/ff/1e4a61f5170a9a1d978f3ac3872449de6c01fc71eaf89657824c878b1549/jupyter_lsp-2.3.1.tar.gz", hash = "sha256:fdf8a4aa7d85813976d6e29e95e6a2c8f752701f926f2715305249a3829805a6", size = 55677, upload-time = "2026-04-02T08:10:06.749Z" } -wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/23/e8/9d61dcbd1dce8ef418f06befd4ac084b4720429c26b0b1222bc218685eff/jupyter_lsp-2.3.1-py3-none-any.whl", hash = "sha256:71b954d834e85ff3096400554f2eefaf7fe37053036f9a782b0f7c5e42dadb81", size = 77513, upload-time = "2026-04-02T08:10:01.753Z" }, -] - -[[package]] -name = "jupyter-server" -version = "2.20.0" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" } -dependencies = [ - { name = "anyio" }, - { name = "argon2-cffi" }, - { name = "jinja2" }, - { name = "jupyter-client" }, - { name = "jupyter-core" }, - { name = "jupyter-events" }, - { name = "jupyter-server-terminals" }, - { name = "nbconvert" }, - { name = "nbformat" }, - { name = "packaging" }, - { name = "prometheus-client" }, - { name = "pywinpty", marker = "os_name == 'nt'" }, - { name = "pyzmq" }, - { name = "send2trash" }, - { name = "terminado" }, - { name = "tornado" }, - { name = "traitlets" }, - { name = "websocket-client" }, -] -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/6b/dc/db3a582633170186f8c8b31298d7eb26ad0eb031a1f53476c258b64eed05/jupyter_server-2.20.0.tar.gz", hash = "sha256:b5778ba337d8015a3dc2b80803ecdd5ac18d3797fddf61a50ea5fb472b4ebe14", size = 756523, upload-time = "2026-06-17T12:09:09.435Z" } -wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f3/71/8c002223e873a870f5c41dc69b0a7c922301123e4a31d5d01ecb700aef77/jupyter_server-2.20.0-py3-none-any.whl", hash = "sha256:c3b67c93c471e947c18b5026f04f21614218adb706df8f48227d3ee8e0a7cdcc", size = 393143, upload-time = "2026-06-17T12:09:07.234Z" }, -] - -[[package]] -name = "jupyter-server-terminals" -version = "0.5.4" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" } -dependencies = [ - { name = "pywinpty", marker = "os_name == 'nt'" }, - { name = "terminado" }, -] -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f4/a7/bcd0a9b0cbba88986fe944aaaf91bfda603e5a50bda8ed15123f381a3b2f/jupyter_server_terminals-0.5.4.tar.gz", hash = "sha256:bbda128ed41d0be9020349f9f1f2a4ab9952a73ed5f5ac9f1419794761fb87f5", size = 31770, upload-time = "2026-01-14T16:53:20.213Z" } -wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d1/2d/6674563f71c6320841fc300911a55143925112a72a883e2ca71fba4c618d/jupyter_server_terminals-0.5.4-py3-none-any.whl", hash = "sha256:55be353fc74a80bc7f3b20e6be50a55a61cd525626f578dcb66a5708e2007d14", size = 13704, upload-time = "2026-01-14T16:53:18.738Z" }, -] - -[[package]] -name = "jupyterlab" -version = "4.6.2" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" } -dependencies = [ - { name = "async-lru" }, - { name = "httpx" }, - { name = "ipykernel" }, - { name = "jinja2" }, - { name = "jupyter-builder" }, - { name = "jupyter-core" }, - { name = "jupyter-lsp" }, - { name = "jupyter-server" }, - { name = "jupyterlab-server" }, - { name = "notebook-shim" }, - { name = "packaging" }, - { name = "tornado" }, - { name = "traitlets" }, -] -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/7a/7f/51c0c856ab286bdaf5709cf61ed13584ed9d4bee906479707da45b11b353/jupyterlab-4.6.2.tar.gz", hash = "sha256:e18ce8b34f3de350e93cd5b2c4f3ae884cbe266eb76bf5d6825a4ed34c13bcff", size = 28183650, upload-time = "2026-07-21T12:05:24.051Z" } -wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/63/1f/e39b248c76bb3736bc05a9491a6aa1315414c32dea12f548ecc1b24c758e/jupyterlab-4.6.2-py3-none-any.whl", hash = "sha256:5964447036629adfcd3fc0969effc1da6f47d2cbd0a60b2c2eea7c31be0ec6a8", size = 17166703, upload-time = "2026-07-21T12:05:19.818Z" }, -] - -[[package]] -name = "jupyterlab-pygments" -version = "0.3.0" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" } -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/90/51/9187be60d989df97f5f0aba133fa54e7300f17616e065d1ada7d7646b6d6/jupyterlab_pygments-0.3.0.tar.gz", hash = "sha256:721aca4d9029252b11cfa9d185e5b5af4d54772bb8072f9b7036f4170054d35d" } -wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b1/dd/ead9d8ea85bf202d90cc513b533f9c363121c7792674f78e0d8a854b63b4/jupyterlab_pygments-0.3.0-py3-none-any.whl", hash = "sha256:841a89020971da1d8693f1a99997aefc5dc424bb1b251fd6322462a1b8842780" }, -] - -[[package]] -name = "jupyterlab-server" -version = "2.28.0" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" } -dependencies = [ - { name = "babel" }, - { name = "jinja2" }, - { name = "json5" }, - { name = "jsonschema" }, - { name = "jupyter-server" }, - { name = "packaging" }, - { name = "requests" }, -] -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d6/2c/90153f189e421e93c4bb4f9e3f59802a1f01abd2ac5cf40b152d7f735232/jupyterlab_server-2.28.0.tar.gz", hash = "sha256:35baa81898b15f93573e2deca50d11ac0ae407ebb688299d3a5213265033712c", size = 76996, upload-time = "2025-10-22T13:59:18.37Z" } -wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e0/07/a000fe835f76b7e1143242ab1122e6362ef1c03f23f83a045c38859c2ae0/jupyterlab_server-2.28.0-py3-none-any.whl", hash = "sha256:e4355b148fdcf34d312bbbc80f22467d6d20460e8b8736bf235577dd18506968", size = 59830, upload-time = "2025-10-22T13:59:16.767Z" }, -] - -[[package]] -name = "lark" -version = "1.3.1" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" } -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/da/34/28fff3ab31ccff1fd4f6c7c7b0ceb2b6968d8ea4950663eadcb5720591a0/lark-1.3.1.tar.gz", hash = "sha256:b426a7a6d6d53189d318f2b6236ab5d6429eaf09259f1ca33eb716eed10d2905", size = 382732, upload-time = "2025-10-27T18:25:56.653Z" } -wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/82/3d/14ce75ef66813643812f3093ab17e46d3a206942ce7376d31ec2d36229e7/lark-1.3.1-py3-none-any.whl", hash = "sha256:c629b661023a014c37da873b4ff58a817398d12635d3bbb2c5a03be7fe5d1e12", size = 113151, upload-time = "2025-10-27T18:25:54.882Z" }, -] - -[[package]] -name = "loguru" -version = "0.7.3" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "win32-setctime", marker = "sys_platform == 'win32'" }, -] -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/3a/05/a1dae3dffd1116099471c643b8924f5aa6524411dc6c63fdae648c4f1aca/loguru-0.7.3.tar.gz", hash = "sha256:19480589e77d47b8d85b2c827ad95d49bf31b0dcde16593892eb51dd18706eb6", size = 63559, upload-time = "2024-12-06T11:20:56.608Z" } -wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/0c/29/0348de65b8cc732daa3e33e67806420b2ae89bdce2b04af740289c5c6c8c/loguru-0.7.3-py3-none-any.whl", hash = "sha256:31a33c10c8e1e10422bfd431aeb5d351c7cf7fa671e3c4df004162264b28220c", size = 61595, upload-time = "2024-12-06T11:20:54.538Z" }, + { url = "https://files.pythonhosted.org/packages/e7/e7/80988e32bf6f73919a113473a604f5a8f09094de312b9d52b79c2df7612b/jupyter_core-5.9.1-py3-none-any.whl", hash = "sha256:ebf87fdc6073d142e114c72c9e29a9d7ca03fad818c5d300ce2adc1fb0743407", size = 29032, upload-time = "2025-10-16T19:19:16.783Z" }, ] [[package]] name = "mako" version = "1.3.12" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" } +source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markupsafe" }, ] -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/00/62/791b31e69ae182791ec67f04850f2f062716bbd205483d63a215f3e062d3/mako-1.3.12.tar.gz", hash = "sha256:9f778e93289bd410bb35daadeb4fc66d95a746f0b75777b942088b7fd7af550a", size = 400219, upload-time = "2026-04-28T19:01:08.512Z" } +sdist = { url = "https://files.pythonhosted.org/packages/00/62/791b31e69ae182791ec67f04850f2f062716bbd205483d63a215f3e062d3/mako-1.3.12.tar.gz", hash = "sha256:9f778e93289bd410bb35daadeb4fc66d95a746f0b75777b942088b7fd7af550a", size = 400219, upload-time = "2026-04-28T19:01:08.512Z" } wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/bc/b1/a0ec7a5a9db730a08daef1fdfb8090435b82465abbf758a596f0ea88727e/mako-1.3.12-py3-none-any.whl", hash = "sha256:8f61569480282dbf557145ce441e4ba888be453c30989f879f0d652e39f53ea9", size = 78521, upload-time = "2026-04-28T19:01:10.393Z" }, + { url = "https://files.pythonhosted.org/packages/bc/b1/a0ec7a5a9db730a08daef1fdfb8090435b82465abbf758a596f0ea88727e/mako-1.3.12-py3-none-any.whl", hash = "sha256:8f61569480282dbf557145ce441e4ba888be453c30989f879f0d652e39f53ea9", size = 78521, upload-time = "2026-04-28T19:01:10.393Z" }, ] [[package]] name = "markupsafe" version = "3.0.3" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" } -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, ] [[package]] name = "matplotlib-inline" version = "0.2.2" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" } +source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "traitlets" }, ] -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/bd/c0/9f7c9a46090390368a4d7bcb76bb87a4a36c421e4c0792cdb53486ffac7a/matplotlib_inline-0.2.2.tar.gz", hash = "sha256:72f3fe8fce36b70d4a5b612f899090cd0401deddc4ea90e1572b9f4bfb058c79", size = 8150, upload-time = "2026-05-08T17:33:33.49Z" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/c0/9f7c9a46090390368a4d7bcb76bb87a4a36c421e4c0792cdb53486ffac7a/matplotlib_inline-0.2.2.tar.gz", hash = "sha256:72f3fe8fce36b70d4a5b612f899090cd0401deddc4ea90e1572b9f4bfb058c79", size = 8150, upload-time = "2026-05-08T17:33:33.49Z" } wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/41/09/5b161152e2d90f7b87f781c2e1267494aef9c32498df793f73ad0a0a494a/matplotlib_inline-0.2.2-py3-none-any.whl", hash = "sha256:3c821cf1c209f59fb2d2d64abbf5b23b67bcb2210d663f9918dd851c6da1fcf6", size = 9534, upload-time = "2026-05-08T17:33:32.055Z" }, -] - -[[package]] -name = "mistune" -version = "3.3.4" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" } -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/7b/92/328a294a6de83bacb95bed01f04e0eaff4e3616ee359fc821a5dfc539b02/mistune-3.3.4.tar.gz", hash = "sha256:58b5c96d6fcb61190dfe5fae498d2b2065f99cf61e9649418fd54cf1ada86dfe", size = 121426, upload-time = "2026-07-22T05:22:30.89Z" } -wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/77/e4/288365afae98953bc01de09f686f40d8ee84578135aa7767d5d4e60b5278/mistune-3.3.4-py3-none-any.whl", hash = "sha256:ee015381e955e370962968befe1d729ab60fafb6a715ac6751763fbce38c8d4a", size = 66862, upload-time = "2026-07-22T05:22:29.419Z" }, + { url = "https://files.pythonhosted.org/packages/41/09/5b161152e2d90f7b87f781c2e1267494aef9c32498df793f73ad0a0a494a/matplotlib_inline-0.2.2-py3-none-any.whl", hash = "sha256:3c821cf1c209f59fb2d2d64abbf5b23b67bcb2210d663f9918dd851c6da1fcf6", size = 9534, upload-time = "2026-05-08T17:33:32.055Z" }, ] [[package]] @@ -1082,925 +789,894 @@ source = { virtual = "." } [[package]] name = "nbclient" -version = "0.11.0" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" } +version = "0.10.2" +source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jupyter-client" }, { name = "jupyter-core" }, { name = "nbformat" }, { name = "traitlets" }, ] -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/28/a5/b3bae4b590c0cbcada2c63a34f7580024e834a8ba213e949a2f906705787/nbclient-0.11.0.tar.gz", hash = "sha256:04a134a5b087f2c5887f228aca155db50169b8cd9334dee6942c8e927e56081a", size = 62535, upload-time = "2026-06-05T07:52:41.746Z" } +sdist = { url = "https://files.pythonhosted.org/packages/87/66/7ffd18d58eae90d5721f9f39212327695b749e23ad44b3881744eaf4d9e8/nbclient-0.10.2.tar.gz", hash = "sha256:90b7fc6b810630db87a6d0c2250b1f0ab4cf4d3c27a299b0cde78a4ed3fd9193", size = 62424, upload-time = "2024-12-19T10:32:27.164Z" } wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/36/c9/94d73e5a01c5b926c3fa2496e97d7a8dc28ed5a77c0b2ed712f1a62e6694/nbclient-0.11.0-py3-none-any.whl", hash = "sha256:ef7fa0d59d6e1d41103933d8a445a18d5de860ca6b613b87b8574accdb3c2895", size = 25288, upload-time = "2026-06-05T07:52:40.115Z" }, -] - -[[package]] -name = "nbconvert" -version = "7.17.1" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" } -dependencies = [ - { name = "beautifulsoup4" }, - { name = "bleach", extra = ["css"] }, - { name = "defusedxml" }, - { name = "jinja2" }, - { name = "jupyter-core" }, - { name = "jupyterlab-pygments" }, - { name = "markupsafe" }, - { name = "mistune" }, - { name = "nbclient" }, - { name = "nbformat" }, - { name = "packaging" }, - { name = "pandocfilters" }, - { name = "pygments" }, - { name = "traitlets" }, -] -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/01/b1/708e53fe2e429c103c6e6e159106bcf0357ac41aa4c28772bd8402339051/nbconvert-7.17.1.tar.gz", hash = "sha256:34d0d0a7e73ce3cbab6c5aae8f4f468797280b01fd8bd2ca746da8569eddd7d2", size = 865311, upload-time = "2026-04-08T00:44:14.914Z" } -wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/67/f8/bb0a9d5f46819c821dc1f004aa2cc29b1d91453297dbf5ff20470f00f193/nbconvert-7.17.1-py3-none-any.whl", hash = "sha256:aa85c087b435e7bf1ffd03319f658e285f2b89eccab33bc1ba7025495ab3e7c8", size = 261927, upload-time = "2026-04-08T00:44:12.845Z" }, + { url = "https://files.pythonhosted.org/packages/34/6d/e7fa07f03a4a7b221d94b4d586edb754a9b0dc3c9e2c93353e9fa4e0d117/nbclient-0.10.2-py3-none-any.whl", hash = "sha256:4ffee11e788b4a27fabeb7955547e4318a5298f34342a4bfd01f2e1faaeadc3d", size = 25434, upload-time = "2024-12-19T10:32:24.139Z" }, ] [[package]] name = "nbformat" version = "5.10.4" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" } +source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "fastjsonschema" }, { name = "jsonschema" }, { name = "jupyter-core" }, { name = "traitlets" }, ] -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/6d/fd/91545e604bc3dad7dca9ed03284086039b294c6b3d75c0d2fa45f9e9caf3/nbformat-5.10.4.tar.gz", hash = "sha256:322168b14f937a5d11362988ecac2a4952d3d8e3a2cbeb2319584631226d5b3a", size = 142749, upload-time = "2024-04-04T11:20:37.371Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6d/fd/91545e604bc3dad7dca9ed03284086039b294c6b3d75c0d2fa45f9e9caf3/nbformat-5.10.4.tar.gz", hash = "sha256:322168b14f937a5d11362988ecac2a4952d3d8e3a2cbeb2319584631226d5b3a", size = 142749, upload-time = "2024-04-04T11:20:37.371Z" } wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a9/82/0340caa499416c78e5d8f5f05947ae4bc3cba53c9f038ab6e9ed964e22f1/nbformat-5.10.4-py3-none-any.whl", hash = "sha256:3b48d6c8fbca4b299bf3982ea7db1af21580e4fec269ad087b9e81588891200b", size = 78454, upload-time = "2024-04-04T11:20:34.895Z" }, + { url = "https://files.pythonhosted.org/packages/a9/82/0340caa499416c78e5d8f5f05947ae4bc3cba53c9f038ab6e9ed964e22f1/nbformat-5.10.4-py3-none-any.whl", hash = "sha256:3b48d6c8fbca4b299bf3982ea7db1af21580e4fec269ad087b9e81588891200b", size = 78454, upload-time = "2024-04-04T11:20:34.895Z" }, ] [[package]] -name = "nest-asyncio2" -version = "1.7.2" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" } -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b4/73/731debf26e27e0a0323d7bda270dc2f634b398e38f040a09da1f4351d0aa/nest_asyncio2-1.7.2.tar.gz", hash = "sha256:1921d70b92cc4612c374928d081552efb59b83d91b2b789d935c665fa01729a8", size = 14743, upload-time = "2026-02-13T00:34:04.386Z" } +name = "nest-asyncio" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/83/f8/51569ac65d696c8ecbee95938f89d4abf00f47d58d48f6fbabfe8f0baefe/nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe", size = 7418, upload-time = "2024-01-21T14:25:19.227Z" } wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c5/3c/3179b85b0e1c3659f0369940200cd6d0fa900e6cefcc7ea0bc6dd0e29ffb/nest_asyncio2-1.7.2-py3-none-any.whl", hash = "sha256:f5dfa702f3f81f6a03857e9a19e2ba578c0946a4ad417b4c50a24d7ba641fe01", size = 7843, upload-time = "2026-02-13T00:34:02.691Z" }, -] - -[[package]] -name = "notebook" -version = "7.6.1" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" } -dependencies = [ - { name = "jupyter-builder" }, - { name = "jupyter-server" }, - { name = "jupyterlab" }, - { name = "jupyterlab-server" }, - { name = "notebook-shim" }, - { name = "tornado" }, -] -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/28/62/34df9b5f6cef6a3e71301cd2f5525fe93a983ae46bd217e7cca27374a037/notebook-7.6.1.tar.gz", hash = "sha256:0b45fd1010668dd4808c40914d957706dbf044a677d28764dc881b74dfcede82", size = 5499443, upload-time = "2026-07-22T12:39:05.432Z" } -wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/3c/19/003ac39eae3a88b80631f93eb954b61d0f1fc1dce4c84602eb0bf4802466/notebook-7.6.1-py3-none-any.whl", hash = "sha256:6ea1e4c926f0dc490444ddcc335797a3dacda470a805d01031872fb119988ba2", size = 5546368, upload-time = "2026-07-22T12:39:02.287Z" }, -] - -[[package]] -name = "notebook-shim" -version = "0.2.4" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" } -dependencies = [ - { name = "jupyter-server" }, -] -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/54/d2/92fa3243712b9a3e8bafaf60aac366da1cada3639ca767ff4b5b3654ec28/notebook_shim-0.2.4.tar.gz", hash = "sha256:b4b2cfa1b65d98307ca24361f5b30fe785b53c3fd07b7a47e89acb5e6ac638cb", size = 13167, upload-time = "2024-02-14T23:35:18.353Z" } -wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f9/33/bd5b9137445ea4b680023eb0469b2bb969d61303dedb2aac6560ff3d14a1/notebook_shim-0.2.4-py3-none-any.whl", hash = "sha256:411a5be4e9dc882a074ccbcae671eda64cceb068767e9a3419096986560e1cef", size = 13307, upload-time = "2024-02-14T23:35:16.286Z" }, + { url = "https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c", size = 5195, upload-time = "2024-01-21T14:25:17.223Z" }, ] [[package]] name = "packaging" version = "26.2" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" } -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, -] - -[[package]] -name = "pandocfilters" -version = "1.5.1" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" } -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/70/6f/3dd4940bbe001c06a65f88e36bad298bc7a0de5036115639926b0c5c0458/pandocfilters-1.5.1.tar.gz", hash = "sha256:002b4a555ee4ebc03f8b66307e287fa492e4a77b4ea14d3f934328297bb4939e", size = 8454, upload-time = "2024-01-18T20:08:13.726Z" } -wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ef/af/4fbc8cab944db5d21b7e2a5b8e9211a03a79852b1157e2c102fcc61ac440/pandocfilters-1.5.1-py2.py3-none-any.whl", hash = "sha256:93be382804a9cdb0a7267585f157e5d1731bbe5545a85b268d6f5fe6232de2bc", size = 8663, upload-time = "2024-01-18T20:08:11.28Z" }, + { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, ] [[package]] name = "parso" version = "0.8.7" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" } -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/30/4b/90c937815137d43ce71ba043cd3566221e9df6b9c805f24b5d138c9d40a7/parso-0.8.7.tar.gz", hash = "sha256:eaaac4c9fdd5e9e8852dc778d2d7405897ec510f2a298071453e5e3a07914bb1", size = 401824, upload-time = "2026-05-01T23:13:02.138Z" } +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/30/4b/90c937815137d43ce71ba043cd3566221e9df6b9c805f24b5d138c9d40a7/parso-0.8.7.tar.gz", hash = "sha256:eaaac4c9fdd5e9e8852dc778d2d7405897ec510f2a298071453e5e3a07914bb1", size = 401824, upload-time = "2026-05-01T23:13:02.138Z" } wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/99/5d/8268b644392ee874ee82a635cd0df1773de230bde356c38de28e298392cc/parso-0.8.7-py2.py3-none-any.whl", hash = "sha256:a8926eb2a1b915486941fdbd31e86a4baf88fe8c210f25f2f35ecec5b574ca1c", size = 107025, upload-time = "2026-05-01T23:12:58.867Z" }, + { url = "https://files.pythonhosted.org/packages/99/5d/8268b644392ee874ee82a635cd0df1773de230bde356c38de28e298392cc/parso-0.8.7-py2.py3-none-any.whl", hash = "sha256:a8926eb2a1b915486941fdbd31e86a4baf88fe8c210f25f2f35ecec5b574ca1c", size = 107025, upload-time = "2026-05-01T23:12:58.867Z" }, ] [[package]] name = "pexpect" version = "4.9.0" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" } +source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "ptyprocess" }, ] -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450, upload-time = "2023-11-25T09:07:26.339Z" } +sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450, upload-time = "2023-11-25T09:07:26.339Z" } wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772, upload-time = "2023-11-25T06:56:14.81Z" }, + { url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772, upload-time = "2023-11-25T06:56:14.81Z" }, ] [[package]] name = "platformdirs" version = "4.11.0" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" } -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/78/9b/560e4be8e26f6fd133a03630a8df0c663b9e8d61b4ade152b72005aec83b/platformdirs-4.11.0.tar.gz", hash = "sha256:0555d18370482847566ffabcaa53ad7c6c1c29f195989ae1ed634a05f76ea1e0", size = 31953, upload-time = "2026-07-21T13:09:36.565Z" } +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/78/9b/560e4be8e26f6fd133a03630a8df0c663b9e8d61b4ade152b72005aec83b/platformdirs-4.11.0.tar.gz", hash = "sha256:0555d18370482847566ffabcaa53ad7c6c1c29f195989ae1ed634a05f76ea1e0", size = 31953, upload-time = "2026-07-21T13:09:36.565Z" } wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/7d/68/d8d58938dfb1370b266a1a729e6d77a985be23689a0496498ee17b2cbf90/platformdirs-4.11.0-py3-none-any.whl", hash = "sha256:360ccded2b7fce0af0ff80cc8f5942a1c5d99b0e856033acb030bfc634709e74", size = 23247, upload-time = "2026-07-21T13:09:35.422Z" }, -] - -[[package]] -name = "prometheus-client" -version = "0.26.0" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" } -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/52/73/f1334c29c2af4cd9dba6c7817e61b611bd0215e2eb5565c6064a4de18802/prometheus_client-0.26.0.tar.gz", hash = "sha256:04a91bcf94e2cf74a44a1a874d651a2e853ed354b6e822f3b7487751465d5c2b", size = 92910, upload-time = "2026-07-24T19:36:41.893Z" } -wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/eb/a3/b69efbf4143b5b9859b977770bbbabcc2796b702fa69dc40271e45cd5a56/prometheus_client-0.26.0-py3-none-any.whl", hash = "sha256:fa93d06737aa02bacd05794768508bb97d2fbee28cb3bca04eaae92f0ca953d6", size = 64494, upload-time = "2026-07-24T19:36:40.854Z" }, + { url = "https://files.pythonhosted.org/packages/7d/68/d8d58938dfb1370b266a1a729e6d77a985be23689a0496498ee17b2cbf90/platformdirs-4.11.0-py3-none-any.whl", hash = "sha256:360ccded2b7fce0af0ff80cc8f5942a1c5d99b0e856033acb030bfc634709e74", size = 23247, upload-time = "2026-07-21T13:09:35.422Z" }, ] [[package]] name = "prompt-toolkit" version = "3.0.53" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" } +source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "wcwidth" }, ] -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/7d/ea/39b988c938f75cb75d7045b5c69f8bfed47ee2152c8837fb403de29d6fb8/prompt_toolkit-3.0.53.tar.gz", hash = "sha256:9ec8a0ad96d5c56148b3f914aa79c1564c3fde5d2e6b876e7bc327e353cf8fa6", size = 435492, upload-time = "2026-07-26T20:56:14.758Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/ea/39b988c938f75cb75d7045b5c69f8bfed47ee2152c8837fb403de29d6fb8/prompt_toolkit-3.0.53.tar.gz", hash = "sha256:9ec8a0ad96d5c56148b3f914aa79c1564c3fde5d2e6b876e7bc327e353cf8fa6", size = 435492, upload-time = "2026-07-26T20:56:14.758Z" } wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/54/6f/84908cad2d6aa5144abcf7b42709fe4fdb459bc640ec7ac5786e7693dabc/prompt_toolkit-3.0.53-py3-none-any.whl", hash = "sha256:01c0891d7f9237d5e339f7d3e42cdae80b7534abb1c7c0e3352efba6231492f2", size = 392288, upload-time = "2026-07-26T20:56:12.512Z" }, + { url = "https://files.pythonhosted.org/packages/54/6f/84908cad2d6aa5144abcf7b42709fe4fdb459bc640ec7ac5786e7693dabc/prompt_toolkit-3.0.53-py3-none-any.whl", hash = "sha256:01c0891d7f9237d5e339f7d3e42cdae80b7534abb1c7c0e3352efba6231492f2", size = 392288, upload-time = "2026-07-26T20:56:12.512Z" }, ] [[package]] name = "psutil" version = "7.2.2" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" } -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/aa/c6/d1ddf4abb55e93cebc4f2ed8b5d6dbad109ecb8d63748dd2b20ab5e57ebe/psutil-7.2.2.tar.gz", hash = "sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372", size = 493740, upload-time = "2026-01-28T18:14:54.428Z" } +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/c6/d1ddf4abb55e93cebc4f2ed8b5d6dbad109ecb8d63748dd2b20ab5e57ebe/psutil-7.2.2.tar.gz", hash = "sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372", size = 493740, upload-time = "2026-01-28T18:14:54.428Z" } wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/51/08/510cbdb69c25a96f4ae523f733cdc963ae654904e8db864c07585ef99875/psutil-7.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2edccc433cbfa046b980b0df0171cd25bcaeb3a68fe9022db0979e7aa74a826b", size = 130595, upload-time = "2026-01-28T18:14:57.293Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d6/f5/97baea3fe7a5a9af7436301f85490905379b1c6f2dd51fe3ecf24b4c5fbf/psutil-7.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e78c8603dcd9a04c7364f1a3e670cea95d51ee865e4efb3556a3a63adef958ea", size = 131082, upload-time = "2026-01-28T18:14:59.732Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/37/d6/246513fbf9fa174af531f28412297dd05241d97a75911ac8febefa1a53c6/psutil-7.2.2-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1a571f2330c966c62aeda00dd24620425d4b0cc86881c89861fbc04549e5dc63", size = 181476, upload-time = "2026-01-28T18:15:01.884Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b8/b5/9182c9af3836cca61696dabe4fd1304e17bc56cb62f17439e1154f225dd3/psutil-7.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:917e891983ca3c1887b4ef36447b1e0873e70c933afc831c6b6da078ba474312", size = 184062, upload-time = "2026-01-28T18:15:04.436Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/16/ba/0756dca669f5a9300d0cbcbfae9a4c30e446dfc7440ffe43ded5724bfd93/psutil-7.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:ab486563df44c17f5173621c7b198955bd6b613fb87c71c161f827d3fb149a9b", size = 139893, upload-time = "2026-01-28T18:15:06.378Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/1c/61/8fa0e26f33623b49949346de05ec1ddaad02ed8ba64af45f40a147dbfa97/psutil-7.2.2-cp313-cp313t-win_arm64.whl", hash = "sha256:ae0aefdd8796a7737eccea863f80f81e468a1e4cf14d926bd9b6f5f2d5f90ca9", size = 135589, upload-time = "2026-01-28T18:15:08.03Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/81/69/ef179ab5ca24f32acc1dac0c247fd6a13b501fd5534dbae0e05a1c48b66d/psutil-7.2.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:eed63d3b4d62449571547b60578c5b2c4bcccc5387148db46e0c2313dad0ee00", size = 130664, upload-time = "2026-01-28T18:15:09.469Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/7b/64/665248b557a236d3fa9efc378d60d95ef56dd0a490c2cd37dafc7660d4a9/psutil-7.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7b6d09433a10592ce39b13d7be5a54fbac1d1228ed29abc880fb23df7cb694c9", size = 131087, upload-time = "2026-01-28T18:15:11.724Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d5/2e/e6782744700d6759ebce3043dcfa661fb61e2fb752b91cdeae9af12c2178/psutil-7.2.2-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fa4ecf83bcdf6e6c8f4449aff98eefb5d0604bf88cb883d7da3d8d2d909546a", size = 182383, upload-time = "2026-01-28T18:15:13.445Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/57/49/0a41cefd10cb7505cdc04dab3eacf24c0c2cb158a998b8c7b1d27ee2c1f5/psutil-7.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e452c464a02e7dc7822a05d25db4cde564444a67e58539a00f929c51eddda0cf", size = 185210, upload-time = "2026-01-28T18:15:16.002Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/dd/2c/ff9bfb544f283ba5f83ba725a3c5fec6d6b10b8f27ac1dc641c473dc390d/psutil-7.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c7663d4e37f13e884d13994247449e9f8f574bc4655d509c3b95e9ec9e2b9dc1", size = 141228, upload-time = "2026-01-28T18:15:18.385Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f2/fc/f8d9c31db14fcec13748d373e668bc3bed94d9077dbc17fb0eebc073233c/psutil-7.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:11fe5a4f613759764e79c65cf11ebdf26e33d6dd34336f8a337aa2996d71c841", size = 136284, upload-time = "2026-01-28T18:15:19.912Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e7/36/5ee6e05c9bd427237b11b3937ad82bb8ad2752d72c6969314590dd0c2f6e/psutil-7.2.2-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486", size = 129090, upload-time = "2026-01-28T18:15:22.168Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/80/c4/f5af4c1ca8c1eeb2e92ccca14ce8effdeec651d5ab6053c589b074eda6e1/psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979", size = 129859, upload-time = "2026-01-28T18:15:23.795Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9", size = 155560, upload-time = "2026-01-28T18:15:25.976Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/63/65/37648c0c158dc222aba51c089eb3bdfa238e621674dc42d48706e639204f/psutil-7.2.2-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e", size = 156997, upload-time = "2026-01-28T18:15:27.794Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/8e/13/125093eadae863ce03c6ffdbae9929430d116a246ef69866dad94da3bfbc/psutil-7.2.2-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8", size = 148972, upload-time = "2026-01-28T18:15:29.342Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/04/78/0acd37ca84ce3ddffaa92ef0f571e073faa6d8ff1f0559ab1272188ea2be/psutil-7.2.2-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc", size = 148266, upload-time = "2026-01-28T18:15:31.597Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b4/90/e2159492b5426be0c1fef7acba807a03511f97c5f86b3caeda6ad92351a7/psutil-7.2.2-cp37-abi3-win_amd64.whl", hash = "sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988", size = 137737, upload-time = "2026-01-28T18:15:33.849Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/8c/c7/7bb2e321574b10df20cbde462a94e2b71d05f9bbda251ef27d104668306a/psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee", size = 134617, upload-time = "2026-01-28T18:15:36.514Z" }, + { url = "https://files.pythonhosted.org/packages/51/08/510cbdb69c25a96f4ae523f733cdc963ae654904e8db864c07585ef99875/psutil-7.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2edccc433cbfa046b980b0df0171cd25bcaeb3a68fe9022db0979e7aa74a826b", size = 130595, upload-time = "2026-01-28T18:14:57.293Z" }, + { url = "https://files.pythonhosted.org/packages/d6/f5/97baea3fe7a5a9af7436301f85490905379b1c6f2dd51fe3ecf24b4c5fbf/psutil-7.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e78c8603dcd9a04c7364f1a3e670cea95d51ee865e4efb3556a3a63adef958ea", size = 131082, upload-time = "2026-01-28T18:14:59.732Z" }, + { url = "https://files.pythonhosted.org/packages/37/d6/246513fbf9fa174af531f28412297dd05241d97a75911ac8febefa1a53c6/psutil-7.2.2-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1a571f2330c966c62aeda00dd24620425d4b0cc86881c89861fbc04549e5dc63", size = 181476, upload-time = "2026-01-28T18:15:01.884Z" }, + { url = "https://files.pythonhosted.org/packages/b8/b5/9182c9af3836cca61696dabe4fd1304e17bc56cb62f17439e1154f225dd3/psutil-7.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:917e891983ca3c1887b4ef36447b1e0873e70c933afc831c6b6da078ba474312", size = 184062, upload-time = "2026-01-28T18:15:04.436Z" }, + { url = "https://files.pythonhosted.org/packages/16/ba/0756dca669f5a9300d0cbcbfae9a4c30e446dfc7440ffe43ded5724bfd93/psutil-7.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:ab486563df44c17f5173621c7b198955bd6b613fb87c71c161f827d3fb149a9b", size = 139893, upload-time = "2026-01-28T18:15:06.378Z" }, + { url = "https://files.pythonhosted.org/packages/1c/61/8fa0e26f33623b49949346de05ec1ddaad02ed8ba64af45f40a147dbfa97/psutil-7.2.2-cp313-cp313t-win_arm64.whl", hash = "sha256:ae0aefdd8796a7737eccea863f80f81e468a1e4cf14d926bd9b6f5f2d5f90ca9", size = 135589, upload-time = "2026-01-28T18:15:08.03Z" }, + { url = "https://files.pythonhosted.org/packages/81/69/ef179ab5ca24f32acc1dac0c247fd6a13b501fd5534dbae0e05a1c48b66d/psutil-7.2.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:eed63d3b4d62449571547b60578c5b2c4bcccc5387148db46e0c2313dad0ee00", size = 130664, upload-time = "2026-01-28T18:15:09.469Z" }, + { url = "https://files.pythonhosted.org/packages/7b/64/665248b557a236d3fa9efc378d60d95ef56dd0a490c2cd37dafc7660d4a9/psutil-7.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7b6d09433a10592ce39b13d7be5a54fbac1d1228ed29abc880fb23df7cb694c9", size = 131087, upload-time = "2026-01-28T18:15:11.724Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2e/e6782744700d6759ebce3043dcfa661fb61e2fb752b91cdeae9af12c2178/psutil-7.2.2-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fa4ecf83bcdf6e6c8f4449aff98eefb5d0604bf88cb883d7da3d8d2d909546a", size = 182383, upload-time = "2026-01-28T18:15:13.445Z" }, + { url = "https://files.pythonhosted.org/packages/57/49/0a41cefd10cb7505cdc04dab3eacf24c0c2cb158a998b8c7b1d27ee2c1f5/psutil-7.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e452c464a02e7dc7822a05d25db4cde564444a67e58539a00f929c51eddda0cf", size = 185210, upload-time = "2026-01-28T18:15:16.002Z" }, + { url = "https://files.pythonhosted.org/packages/dd/2c/ff9bfb544f283ba5f83ba725a3c5fec6d6b10b8f27ac1dc641c473dc390d/psutil-7.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c7663d4e37f13e884d13994247449e9f8f574bc4655d509c3b95e9ec9e2b9dc1", size = 141228, upload-time = "2026-01-28T18:15:18.385Z" }, + { url = "https://files.pythonhosted.org/packages/f2/fc/f8d9c31db14fcec13748d373e668bc3bed94d9077dbc17fb0eebc073233c/psutil-7.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:11fe5a4f613759764e79c65cf11ebdf26e33d6dd34336f8a337aa2996d71c841", size = 136284, upload-time = "2026-01-28T18:15:19.912Z" }, + { url = "https://files.pythonhosted.org/packages/e7/36/5ee6e05c9bd427237b11b3937ad82bb8ad2752d72c6969314590dd0c2f6e/psutil-7.2.2-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486", size = 129090, upload-time = "2026-01-28T18:15:22.168Z" }, + { url = "https://files.pythonhosted.org/packages/80/c4/f5af4c1ca8c1eeb2e92ccca14ce8effdeec651d5ab6053c589b074eda6e1/psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979", size = 129859, upload-time = "2026-01-28T18:15:23.795Z" }, + { url = "https://files.pythonhosted.org/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9", size = 155560, upload-time = "2026-01-28T18:15:25.976Z" }, + { url = "https://files.pythonhosted.org/packages/63/65/37648c0c158dc222aba51c089eb3bdfa238e621674dc42d48706e639204f/psutil-7.2.2-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e", size = 156997, upload-time = "2026-01-28T18:15:27.794Z" }, + { url = "https://files.pythonhosted.org/packages/8e/13/125093eadae863ce03c6ffdbae9929430d116a246ef69866dad94da3bfbc/psutil-7.2.2-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8", size = 148972, upload-time = "2026-01-28T18:15:29.342Z" }, + { url = "https://files.pythonhosted.org/packages/04/78/0acd37ca84ce3ddffaa92ef0f571e073faa6d8ff1f0559ab1272188ea2be/psutil-7.2.2-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc", size = 148266, upload-time = "2026-01-28T18:15:31.597Z" }, + { url = "https://files.pythonhosted.org/packages/b4/90/e2159492b5426be0c1fef7acba807a03511f97c5f86b3caeda6ad92351a7/psutil-7.2.2-cp37-abi3-win_amd64.whl", hash = "sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988", size = 137737, upload-time = "2026-01-28T18:15:33.849Z" }, + { url = "https://files.pythonhosted.org/packages/8c/c7/7bb2e321574b10df20cbde462a94e2b71d05f9bbda251ef27d104668306a/psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee", size = 134617, upload-time = "2026-01-28T18:15:36.514Z" }, ] [[package]] name = "ptyprocess" version = "0.7.0" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" } -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/20/e5/16ff212c1e452235a90aeb09066144d0c5a6a8c0834397e03f5224495c4e/ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220", size = 70762, upload-time = "2020-12-28T15:15:30.155Z" } +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/e5/16ff212c1e452235a90aeb09066144d0c5a6a8c0834397e03f5224495c4e/ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220", size = 70762, upload-time = "2020-12-28T15:15:30.155Z" } wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", size = 13993, upload-time = "2020-12-28T15:15:28.35Z" }, + { url = "https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", size = 13993, upload-time = "2020-12-28T15:15:28.35Z" }, ] [[package]] name = "pure-eval" version = "0.2.3" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" } -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/cd/05/0a34433a064256a578f1783a10da6df098ceaa4a57bbeaa96a6c0352786b/pure_eval-0.2.3.tar.gz", hash = "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42", size = 19752, upload-time = "2024-07-21T12:58:21.801Z" } +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/05/0a34433a064256a578f1783a10da6df098ceaa4a57bbeaa96a6c0352786b/pure_eval-0.2.3.tar.gz", hash = "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42", size = 19752, upload-time = "2024-07-21T12:58:21.801Z" } wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0", size = 11842, upload-time = "2024-07-21T12:58:20.04Z" }, + { url = "https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0", size = 11842, upload-time = "2024-07-21T12:58:20.04Z" }, ] [[package]] name = "pycparser" version = "3.0" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" } -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, ] [[package]] name = "pydantic" version = "2.13.4" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" } +source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-types" }, { name = "pydantic-core" }, { name = "typing-extensions" }, { name = "typing-inspection" }, ] -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" } +sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" } wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" }, + { url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" }, ] [[package]] name = "pydantic-core" version = "2.46.4" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" } +source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" } wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ce/8c/af022f0af448d7747c5154288d46b5f2bc5f17366eaa0e23e9aa04d59f3b/pydantic_core-2.46.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2", size = 2106158, upload-time = "2026-05-06T13:38:57.215Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/19/95/6195171e385007300f0f5574592e467c568becce2d937a0b6804f218bc49/pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f", size = 1951724, upload-time = "2026-05-06T13:37:02.697Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/8e/bc/f47d1ff9cbb1620e1b5b697eef06010035735f07820180e74178226b27b3/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7", size = 1975742, upload-time = "2026-05-06T13:37:09.448Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/5b/11/9b9a5b0306345664a2da6410877af6e8082481b5884b3ddd78d47c6013ce/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7", size = 2052418, upload-time = "2026-05-06T13:37:38.234Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f1/b7/a65fec226f5d78fc39f4a13c4cc0c768c22b113438f60c14adc9d2865038/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712", size = 2232274, upload-time = "2026-05-06T13:38:27.753Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/68/f0/92039db98b907ef49269a8271f67db9cb78ae2fc68062ef7e4e77adb5f61/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4", size = 2309940, upload-time = "2026-05-06T13:38:05.353Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/5f/97/2aab507d3d00ca626e8e57c1eac6a79e4e5fbcc63eb99733ff55d1717f65/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce", size = 2094516, upload-time = "2026-05-06T13:39:10.577Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/22/37/a8aca44d40d737dde2bc05b3c6c07dff0de07ce6f82e9f3167aeaf4d5dea/pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987", size = 2136854, upload-time = "2026-05-06T13:40:22.59Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/24/99/fcef1b79238c06a8cbec70819ac722ba76e02bc8ada9b0fd66eba40da01b/pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b", size = 2180306, upload-time = "2026-05-06T13:40:10.666Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ae/6c/fc44000918855b42779d007ae63b0532794739027b2f417321cddbc44f6a/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458", size = 2190044, upload-time = "2026-05-06T13:40:43.231Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/6b/65/d9cadc9f1920d7a127ad2edba16c1db7916e59719285cd6c94600b0080ba/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b", size = 2329133, upload-time = "2026-05-06T13:39:57.365Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d0/cf/c873d91679f3a30bcf5e7ac280ce5573483e72295307685120d0d5ad3416/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c", size = 2374464, upload-time = "2026-05-06T13:38:06.976Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/47/bd/6f2fc8188f31bf10590f1e98e7b306336161fac930a8c514cd7bd828c7dc/pydantic_core-2.46.4-cp312-cp312-win32.whl", hash = "sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894", size = 1974823, upload-time = "2026-05-06T13:40:47.985Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/40/8c/985c1d41ea1107c2534abd9870e4ed5c8e7669b5c308297835c001e7a1c4/pydantic_core-2.46.4-cp312-cp312-win_amd64.whl", hash = "sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89", size = 2072919, upload-time = "2026-05-06T13:39:21.153Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c4/ba/f463d006e0c47373ca7ec5e1a261c59dc01ef4d62b2657af925fb0deee3a/pydantic_core-2.46.4-cp312-cp312-win_arm64.whl", hash = "sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a", size = 2027604, upload-time = "2026-05-06T13:39:03.753Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/51/a2/5d30b469c5267a17b39dec53208222f76a8d351dfac4af661888c5aee77d/pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008", size = 2106306, upload-time = "2026-05-06T13:37:48.029Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c1/81/4fa520eaffa8bd7d1525e644cd6d39e7d60b1592bc5b516693c7340b50f1/pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4", size = 1951906, upload-time = "2026-05-06T13:37:17.012Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/03/d5/fd02da45b659668b05923b17ba3a0100a0a3d5541e3bd8fcc4ecb711309e/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76", size = 1976802, upload-time = "2026-05-06T13:37:35.113Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/21/f2/95727e1368be3d3ed485eaab7adbd7dda408f33f7a36e8b48e0144002b91/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3", size = 2052446, upload-time = "2026-05-06T13:37:12.313Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/9c/86/5d99feea3f77c7234b8718075b23db11532773c1a0dbd9b9490215dc2eeb/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76", size = 2232757, upload-time = "2026-05-06T13:39:01.149Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d2/3a/508ac615935ef7588cf6d9e9b91309fdc2da751af865e02a9098de88258c/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4", size = 2309275, upload-time = "2026-05-06T13:37:41.406Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/07/f8/41db9de19d7987d6b04715a02b3b40aea467000275d9d758ffaa31af7d50/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a", size = 2094467, upload-time = "2026-05-06T13:39:18.847Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/2c/e2/f35033184cb11d0052daf4416e8e10a502ea2ac006fc4f459aee872727d1/pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262", size = 2134417, upload-time = "2026-05-06T13:40:17.944Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/7e/7b/6ceeb1cc90e193862f444ebe373d8fdf613f0a82572dde03fb10734c6c71/pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e", size = 2179782, upload-time = "2026-05-06T13:40:32.618Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/5a/f2/c8d7773ede6af08036423a00ae0ceffce266c3c52a096c435d68c896083f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd", size = 2188782, upload-time = "2026-05-06T13:36:51.018Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/59/31/0c864784e31f09f05cdd87606f08923b9c9e7f6e51dd27f20f62f975ce9f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be", size = 2328334, upload-time = "2026-05-06T13:40:37.764Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c2/eb/4f6c8a41efa30baa755590f4141abf3a8c370fab610915733e74134a7270/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d", size = 2372986, upload-time = "2026-05-06T13:39:34.152Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/5b/24/b375a480d53113860c299764bfe9f349a3dc9108b3adc0d7f0d786492ebf/pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb", size = 1973693, upload-time = "2026-05-06T13:37:55.072Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/7e/e8/cff247591966f2d22ec8c003cd7587e27b7ba7b81ab2fb888e3ab75dc285/pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292", size = 2071819, upload-time = "2026-05-06T13:38:49.139Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c6/1a/f4aee670d5670e9e148e0c82c7db98d780be566c6e6a97ee8035528ca0b3/pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d", size = 2027411, upload-time = "2026-05-06T13:40:45.796Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/8d/74/228a26ddad29c6672b805d9fd78e8d251cd04004fa7eed0e622096cd0250/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb", size = 2102079, upload-time = "2026-05-06T13:38:41.019Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462", size = 1952179, upload-time = "2026-05-06T13:36:59.812Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/95/30/5211a831ae054928054b2f79731661087a2bc5c01e825c672b3a4a8f1b3e/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9", size = 1978926, upload-time = "2026-05-06T13:37:39.933Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/57/e9/689668733b1eb67adeef047db3c2e8788fcf65a7fd9c9e2b46b7744fe245/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4", size = 2046785, upload-time = "2026-05-06T13:38:01.995Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/60/d9/6715260422ff50a2109878fd24d948a6c3446bb2664f34ee78cd972b3acd/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914", size = 2228733, upload-time = "2026-05-06T13:40:50.371Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/18/ae/fdb2f64316afca925640f8e70bb1a564b0ec2721c1389e25b8eb4bf9a299/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28", size = 2307534, upload-time = "2026-05-06T13:37:21.531Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b", size = 2099732, upload-time = "2026-05-06T13:39:31.942Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/06/d5/ee5a3366637fee41dee51a1fc91562dcf12ddbc68fda34e6b253da2324bb/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c", size = 2129627, upload-time = "2026-05-06T13:37:25.033Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/94/33/2414be571d2c6a6c4d08be21f9292b6d3fdb08949a97b6dfe985017821db/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb", size = 2179141, upload-time = "2026-05-06T13:37:14.046Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/7b/79/7daa95be995be0eecc4cf75064cb33f9bbbfe3fe0158caf2f0d4a996a5c7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898", size = 2184325, upload-time = "2026-05-06T13:36:53.615Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/9f/cb/d0a382f5c0de8a222dc61c65348e0ce831b1f68e0a018450d31c2cace3a5/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e", size = 2323990, upload-time = "2026-05-06T13:40:29.971Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/05/db/d9ba624cc4a5aced1598e88c04fdbd8310c8a69b9d38b9a3d39ce3a61ed7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519", size = 2369978, upload-time = "2026-05-06T13:37:23.027Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f2/20/d15df15ba918c423461905802bfd2981c3af0bfa0e40d05e13edbfa48bc3/pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4", size = 1966354, upload-time = "2026-05-06T13:38:03.499Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac", size = 2072238, upload-time = "2026-05-06T13:39:40.807Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/32/36/51eb763beec1f4cf59b1db243a7dcc39cbb41230f050a09b9d69faaf0a48/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a", size = 2018251, upload-time = "2026-05-06T13:37:26.72Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e8/91/855af51d625b23aa987116a19e231d2aaef9c4a415273ddc189b79a45fee/pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0", size = 2099593, upload-time = "2026-05-06T13:39:47.682Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/fb/1b/8784a54c65edb5f49f0a14d6977cf1b209bba85a4c77445b255c2de58ab3/pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d", size = 1935226, upload-time = "2026-05-06T13:40:40.428Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e8/e7/1955d28d1afc56dd4b3ad7cc0cf39df1b9852964cf16e5d13912756d6d6b/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b", size = 1974605, upload-time = "2026-05-06T13:37:32.029Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/93/e2/3fedbf0ba7a22850e6e9fd78117f1c0f10f950182344d8a6c535d468fdd8/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000", size = 2030777, upload-time = "2026-05-06T13:38:55.239Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f8/61/46be275fcaaba0b4f5b9669dd852267ce1ff616592dccf7a7845588df091/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e", size = 2236641, upload-time = "2026-05-06T13:37:08.096Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/60/db/12e93e46a8bac9988be3c016860f83293daea8c716c029c9ace279036f2f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd", size = 2286404, upload-time = "2026-05-06T13:40:20.221Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e2/4a/4d8b19008f38d31c53b8219cfedc2e3d5de5fe99d90076b7e767de29274f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3", size = 2109219, upload-time = "2026-05-06T13:38:12.153Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/88/70/3cbc40978fefb7bb09c6708d40d4ad1a5d70fd7213c3d17f971de868ec1f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7", size = 2110594, upload-time = "2026-05-06T13:40:02.971Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/9d/20/b8d36736216e29491125531685b2f9e61aa5b4b2599893f8268551da3338/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff", size = 2159542, upload-time = "2026-05-06T13:39:27.506Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/1d/a2/367df868eb584dacf6bf82a389272406d7178e301c4ac82545ab98bc2dd9/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424", size = 2168146, upload-time = "2026-05-06T13:38:31.93Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c1/b8/4460f77f7e201893f649a29ab355dddd3beee8a97bcb1a320db414f9a06e/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6", size = 2306309, upload-time = "2026-05-06T13:37:44.717Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/64/c4/be2639293acd87dc8ddbcec41a73cee9b2ebf996fe6d892a1a74e88ad3f7/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565", size = 2369736, upload-time = "2026-05-06T13:37:05.645Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02", size = 1955575, upload-time = "2026-05-06T13:38:51.116Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5", size = 2051624, upload-time = "2026-05-06T13:38:21.672Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325, upload-time = "2026-05-06T13:40:52.723Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/9d/1d/8987ad40f65ae1432753072f214fb5c74fe47ffbd0698bb9cbbb585664f8/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7", size = 2095527, upload-time = "2026-05-06T13:39:52.283Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/64/d3/84c282a7eee1d3ac4c0377546ef5a1ea436ce26840d9ac3b7ed54a377507/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df", size = 1936024, upload-time = "2026-05-06T13:40:15.671Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d7/ca/eac61596cdeb4d7e174d3dc0bd8a6238f14f75f97a24e7b7db4c7e7340a0/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526", size = 1990696, upload-time = "2026-05-06T13:38:34.717Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/fa/c3/7c8b240552251faf6b3a957db200fcfbbcec36763c050428b601e0c9b83b/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0", size = 2147590, upload-time = "2026-05-06T13:39:29.883Z" }, -] - -[[package]] -name = "pydantic-settings" -version = "2.14.2" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" } -dependencies = [ - { name = "pydantic" }, - { name = "python-dotenv" }, - { name = "typing-inspection" }, -] -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/5c/b5/8f48e906c3e0205276e8bd8cb7512217a87b2685304d64be27cad5b3019f/pydantic_settings-2.14.2.tar.gz", hash = "sha256:c19dd64b19097f1de80184f0cc7b0272a13ae6e170cbf240a3e27e381ed14a5f", size = 237700, upload-time = "2026-06-19T13:44:56.324Z" } -wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/77/c1/6e422f34e569cf8e18df68d1939c81c099d2b61e4f7d9621c8a77560799c/pydantic_settings-2.14.2-py3-none-any.whl", hash = "sha256:a20c97b37910b6550d5ea50fbcc2d4187defe58cd57070b73863d069419c9440", size = 61715, upload-time = "2026-06-19T13:44:55.02Z" }, + { url = "https://files.pythonhosted.org/packages/ce/8c/af022f0af448d7747c5154288d46b5f2bc5f17366eaa0e23e9aa04d59f3b/pydantic_core-2.46.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2", size = 2106158, upload-time = "2026-05-06T13:38:57.215Z" }, + { url = "https://files.pythonhosted.org/packages/19/95/6195171e385007300f0f5574592e467c568becce2d937a0b6804f218bc49/pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f", size = 1951724, upload-time = "2026-05-06T13:37:02.697Z" }, + { url = "https://files.pythonhosted.org/packages/8e/bc/f47d1ff9cbb1620e1b5b697eef06010035735f07820180e74178226b27b3/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7", size = 1975742, upload-time = "2026-05-06T13:37:09.448Z" }, + { url = "https://files.pythonhosted.org/packages/5b/11/9b9a5b0306345664a2da6410877af6e8082481b5884b3ddd78d47c6013ce/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7", size = 2052418, upload-time = "2026-05-06T13:37:38.234Z" }, + { url = "https://files.pythonhosted.org/packages/f1/b7/a65fec226f5d78fc39f4a13c4cc0c768c22b113438f60c14adc9d2865038/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712", size = 2232274, upload-time = "2026-05-06T13:38:27.753Z" }, + { url = "https://files.pythonhosted.org/packages/68/f0/92039db98b907ef49269a8271f67db9cb78ae2fc68062ef7e4e77adb5f61/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4", size = 2309940, upload-time = "2026-05-06T13:38:05.353Z" }, + { url = "https://files.pythonhosted.org/packages/5f/97/2aab507d3d00ca626e8e57c1eac6a79e4e5fbcc63eb99733ff55d1717f65/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce", size = 2094516, upload-time = "2026-05-06T13:39:10.577Z" }, + { url = "https://files.pythonhosted.org/packages/22/37/a8aca44d40d737dde2bc05b3c6c07dff0de07ce6f82e9f3167aeaf4d5dea/pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987", size = 2136854, upload-time = "2026-05-06T13:40:22.59Z" }, + { url = "https://files.pythonhosted.org/packages/24/99/fcef1b79238c06a8cbec70819ac722ba76e02bc8ada9b0fd66eba40da01b/pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b", size = 2180306, upload-time = "2026-05-06T13:40:10.666Z" }, + { url = "https://files.pythonhosted.org/packages/ae/6c/fc44000918855b42779d007ae63b0532794739027b2f417321cddbc44f6a/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458", size = 2190044, upload-time = "2026-05-06T13:40:43.231Z" }, + { url = "https://files.pythonhosted.org/packages/6b/65/d9cadc9f1920d7a127ad2edba16c1db7916e59719285cd6c94600b0080ba/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b", size = 2329133, upload-time = "2026-05-06T13:39:57.365Z" }, + { url = "https://files.pythonhosted.org/packages/d0/cf/c873d91679f3a30bcf5e7ac280ce5573483e72295307685120d0d5ad3416/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c", size = 2374464, upload-time = "2026-05-06T13:38:06.976Z" }, + { url = "https://files.pythonhosted.org/packages/47/bd/6f2fc8188f31bf10590f1e98e7b306336161fac930a8c514cd7bd828c7dc/pydantic_core-2.46.4-cp312-cp312-win32.whl", hash = "sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894", size = 1974823, upload-time = "2026-05-06T13:40:47.985Z" }, + { url = "https://files.pythonhosted.org/packages/40/8c/985c1d41ea1107c2534abd9870e4ed5c8e7669b5c308297835c001e7a1c4/pydantic_core-2.46.4-cp312-cp312-win_amd64.whl", hash = "sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89", size = 2072919, upload-time = "2026-05-06T13:39:21.153Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ba/f463d006e0c47373ca7ec5e1a261c59dc01ef4d62b2657af925fb0deee3a/pydantic_core-2.46.4-cp312-cp312-win_arm64.whl", hash = "sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a", size = 2027604, upload-time = "2026-05-06T13:39:03.753Z" }, + { url = "https://files.pythonhosted.org/packages/51/a2/5d30b469c5267a17b39dec53208222f76a8d351dfac4af661888c5aee77d/pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008", size = 2106306, upload-time = "2026-05-06T13:37:48.029Z" }, + { url = "https://files.pythonhosted.org/packages/c1/81/4fa520eaffa8bd7d1525e644cd6d39e7d60b1592bc5b516693c7340b50f1/pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4", size = 1951906, upload-time = "2026-05-06T13:37:17.012Z" }, + { url = "https://files.pythonhosted.org/packages/03/d5/fd02da45b659668b05923b17ba3a0100a0a3d5541e3bd8fcc4ecb711309e/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76", size = 1976802, upload-time = "2026-05-06T13:37:35.113Z" }, + { url = "https://files.pythonhosted.org/packages/21/f2/95727e1368be3d3ed485eaab7adbd7dda408f33f7a36e8b48e0144002b91/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3", size = 2052446, upload-time = "2026-05-06T13:37:12.313Z" }, + { url = "https://files.pythonhosted.org/packages/9c/86/5d99feea3f77c7234b8718075b23db11532773c1a0dbd9b9490215dc2eeb/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76", size = 2232757, upload-time = "2026-05-06T13:39:01.149Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3a/508ac615935ef7588cf6d9e9b91309fdc2da751af865e02a9098de88258c/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4", size = 2309275, upload-time = "2026-05-06T13:37:41.406Z" }, + { url = "https://files.pythonhosted.org/packages/07/f8/41db9de19d7987d6b04715a02b3b40aea467000275d9d758ffaa31af7d50/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a", size = 2094467, upload-time = "2026-05-06T13:39:18.847Z" }, + { url = "https://files.pythonhosted.org/packages/2c/e2/f35033184cb11d0052daf4416e8e10a502ea2ac006fc4f459aee872727d1/pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262", size = 2134417, upload-time = "2026-05-06T13:40:17.944Z" }, + { url = "https://files.pythonhosted.org/packages/7e/7b/6ceeb1cc90e193862f444ebe373d8fdf613f0a82572dde03fb10734c6c71/pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e", size = 2179782, upload-time = "2026-05-06T13:40:32.618Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f2/c8d7773ede6af08036423a00ae0ceffce266c3c52a096c435d68c896083f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd", size = 2188782, upload-time = "2026-05-06T13:36:51.018Z" }, + { url = "https://files.pythonhosted.org/packages/59/31/0c864784e31f09f05cdd87606f08923b9c9e7f6e51dd27f20f62f975ce9f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be", size = 2328334, upload-time = "2026-05-06T13:40:37.764Z" }, + { url = "https://files.pythonhosted.org/packages/c2/eb/4f6c8a41efa30baa755590f4141abf3a8c370fab610915733e74134a7270/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d", size = 2372986, upload-time = "2026-05-06T13:39:34.152Z" }, + { url = "https://files.pythonhosted.org/packages/5b/24/b375a480d53113860c299764bfe9f349a3dc9108b3adc0d7f0d786492ebf/pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb", size = 1973693, upload-time = "2026-05-06T13:37:55.072Z" }, + { url = "https://files.pythonhosted.org/packages/7e/e8/cff247591966f2d22ec8c003cd7587e27b7ba7b81ab2fb888e3ab75dc285/pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292", size = 2071819, upload-time = "2026-05-06T13:38:49.139Z" }, + { url = "https://files.pythonhosted.org/packages/c6/1a/f4aee670d5670e9e148e0c82c7db98d780be566c6e6a97ee8035528ca0b3/pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d", size = 2027411, upload-time = "2026-05-06T13:40:45.796Z" }, + { url = "https://files.pythonhosted.org/packages/8d/74/228a26ddad29c6672b805d9fd78e8d251cd04004fa7eed0e622096cd0250/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb", size = 2102079, upload-time = "2026-05-06T13:38:41.019Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462", size = 1952179, upload-time = "2026-05-06T13:36:59.812Z" }, + { url = "https://files.pythonhosted.org/packages/95/30/5211a831ae054928054b2f79731661087a2bc5c01e825c672b3a4a8f1b3e/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9", size = 1978926, upload-time = "2026-05-06T13:37:39.933Z" }, + { url = "https://files.pythonhosted.org/packages/57/e9/689668733b1eb67adeef047db3c2e8788fcf65a7fd9c9e2b46b7744fe245/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4", size = 2046785, upload-time = "2026-05-06T13:38:01.995Z" }, + { url = "https://files.pythonhosted.org/packages/60/d9/6715260422ff50a2109878fd24d948a6c3446bb2664f34ee78cd972b3acd/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914", size = 2228733, upload-time = "2026-05-06T13:40:50.371Z" }, + { url = "https://files.pythonhosted.org/packages/18/ae/fdb2f64316afca925640f8e70bb1a564b0ec2721c1389e25b8eb4bf9a299/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28", size = 2307534, upload-time = "2026-05-06T13:37:21.531Z" }, + { url = "https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b", size = 2099732, upload-time = "2026-05-06T13:39:31.942Z" }, + { url = "https://files.pythonhosted.org/packages/06/d5/ee5a3366637fee41dee51a1fc91562dcf12ddbc68fda34e6b253da2324bb/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c", size = 2129627, upload-time = "2026-05-06T13:37:25.033Z" }, + { url = "https://files.pythonhosted.org/packages/94/33/2414be571d2c6a6c4d08be21f9292b6d3fdb08949a97b6dfe985017821db/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb", size = 2179141, upload-time = "2026-05-06T13:37:14.046Z" }, + { url = "https://files.pythonhosted.org/packages/7b/79/7daa95be995be0eecc4cf75064cb33f9bbbfe3fe0158caf2f0d4a996a5c7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898", size = 2184325, upload-time = "2026-05-06T13:36:53.615Z" }, + { url = "https://files.pythonhosted.org/packages/9f/cb/d0a382f5c0de8a222dc61c65348e0ce831b1f68e0a018450d31c2cace3a5/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e", size = 2323990, upload-time = "2026-05-06T13:40:29.971Z" }, + { url = "https://files.pythonhosted.org/packages/05/db/d9ba624cc4a5aced1598e88c04fdbd8310c8a69b9d38b9a3d39ce3a61ed7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519", size = 2369978, upload-time = "2026-05-06T13:37:23.027Z" }, + { url = "https://files.pythonhosted.org/packages/f2/20/d15df15ba918c423461905802bfd2981c3af0bfa0e40d05e13edbfa48bc3/pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4", size = 1966354, upload-time = "2026-05-06T13:38:03.499Z" }, + { url = "https://files.pythonhosted.org/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac", size = 2072238, upload-time = "2026-05-06T13:39:40.807Z" }, + { url = "https://files.pythonhosted.org/packages/32/36/51eb763beec1f4cf59b1db243a7dcc39cbb41230f050a09b9d69faaf0a48/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a", size = 2018251, upload-time = "2026-05-06T13:37:26.72Z" }, + { url = "https://files.pythonhosted.org/packages/e8/91/855af51d625b23aa987116a19e231d2aaef9c4a415273ddc189b79a45fee/pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0", size = 2099593, upload-time = "2026-05-06T13:39:47.682Z" }, + { url = "https://files.pythonhosted.org/packages/fb/1b/8784a54c65edb5f49f0a14d6977cf1b209bba85a4c77445b255c2de58ab3/pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d", size = 1935226, upload-time = "2026-05-06T13:40:40.428Z" }, + { url = "https://files.pythonhosted.org/packages/e8/e7/1955d28d1afc56dd4b3ad7cc0cf39df1b9852964cf16e5d13912756d6d6b/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b", size = 1974605, upload-time = "2026-05-06T13:37:32.029Z" }, + { url = "https://files.pythonhosted.org/packages/93/e2/3fedbf0ba7a22850e6e9fd78117f1c0f10f950182344d8a6c535d468fdd8/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000", size = 2030777, upload-time = "2026-05-06T13:38:55.239Z" }, + { url = "https://files.pythonhosted.org/packages/f8/61/46be275fcaaba0b4f5b9669dd852267ce1ff616592dccf7a7845588df091/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e", size = 2236641, upload-time = "2026-05-06T13:37:08.096Z" }, + { url = "https://files.pythonhosted.org/packages/60/db/12e93e46a8bac9988be3c016860f83293daea8c716c029c9ace279036f2f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd", size = 2286404, upload-time = "2026-05-06T13:40:20.221Z" }, + { url = "https://files.pythonhosted.org/packages/e2/4a/4d8b19008f38d31c53b8219cfedc2e3d5de5fe99d90076b7e767de29274f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3", size = 2109219, upload-time = "2026-05-06T13:38:12.153Z" }, + { url = "https://files.pythonhosted.org/packages/88/70/3cbc40978fefb7bb09c6708d40d4ad1a5d70fd7213c3d17f971de868ec1f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7", size = 2110594, upload-time = "2026-05-06T13:40:02.971Z" }, + { url = "https://files.pythonhosted.org/packages/9d/20/b8d36736216e29491125531685b2f9e61aa5b4b2599893f8268551da3338/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff", size = 2159542, upload-time = "2026-05-06T13:39:27.506Z" }, + { url = "https://files.pythonhosted.org/packages/1d/a2/367df868eb584dacf6bf82a389272406d7178e301c4ac82545ab98bc2dd9/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424", size = 2168146, upload-time = "2026-05-06T13:38:31.93Z" }, + { url = "https://files.pythonhosted.org/packages/c1/b8/4460f77f7e201893f649a29ab355dddd3beee8a97bcb1a320db414f9a06e/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6", size = 2306309, upload-time = "2026-05-06T13:37:44.717Z" }, + { url = "https://files.pythonhosted.org/packages/64/c4/be2639293acd87dc8ddbcec41a73cee9b2ebf996fe6d892a1a74e88ad3f7/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565", size = 2369736, upload-time = "2026-05-06T13:37:05.645Z" }, + { url = "https://files.pythonhosted.org/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02", size = 1955575, upload-time = "2026-05-06T13:38:51.116Z" }, + { url = "https://files.pythonhosted.org/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5", size = 2051624, upload-time = "2026-05-06T13:38:21.672Z" }, + { url = "https://files.pythonhosted.org/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325, upload-time = "2026-05-06T13:40:52.723Z" }, + { url = "https://files.pythonhosted.org/packages/9d/1d/8987ad40f65ae1432753072f214fb5c74fe47ffbd0698bb9cbbb585664f8/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7", size = 2095527, upload-time = "2026-05-06T13:39:52.283Z" }, + { url = "https://files.pythonhosted.org/packages/64/d3/84c282a7eee1d3ac4c0377546ef5a1ea436ce26840d9ac3b7ed54a377507/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df", size = 1936024, upload-time = "2026-05-06T13:40:15.671Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ca/eac61596cdeb4d7e174d3dc0bd8a6238f14f75f97a24e7b7db4c7e7340a0/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526", size = 1990696, upload-time = "2026-05-06T13:38:34.717Z" }, + { url = "https://files.pythonhosted.org/packages/fa/c3/7c8b240552251faf6b3a957db200fcfbbcec36763c050428b601e0c9b83b/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0", size = 2147590, upload-time = "2026-05-06T13:39:29.883Z" }, ] [[package]] name = "pygments" version = "2.20.0" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" } -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, -] - -[[package]] -name = "pymysql" -version = "1.2.0" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" } -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c9/bc/1c6a92f385940f727daeecf3bacaf186e03875dff57197801046c583bcf0/pymysql-1.2.0.tar.gz", hash = "sha256:6c7b17ca686988104d7426c27895b455cdeea3e9d3ceb1270f0c3704fead8c33", size = 49021, upload-time = "2026-05-19T08:26:22.302Z" } -wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c4/bd/2534e130295c8cfd4f0a2e31623baab7502278f1e97bcfe61db75656a77f/pymysql-1.2.0-py3-none-any.whl", hash = "sha256:62169ce6d5510f08e140c5e7990ee884a9764024e4a9a27b2cc11f1099322ae0", size = 45716, upload-time = "2026-05-19T08:26:20.974Z" }, + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, ] [[package]] name = "python-dateutil" version = "2.9.0.post0" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" } +source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "six" }, ] -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, ] [[package]] name = "python-dotenv" version = "1.2.2" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" } -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" } +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" } wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/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]] -name = "python-json-logger" -version = "4.1.0" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" } -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f7/ff/3cc9165fd44106973cd7ac9facb674a65ed853494592541d339bdc9a30eb/python_json_logger-4.1.0.tar.gz", hash = "sha256:b396b9e3ed782b09ff9d6e4f1683d46c83ad0d35d2e407c09a9ebbf038f88195", size = 17573, upload-time = "2026-03-29T04:39:56.805Z" } -wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/27/be/0631a861af4d1c875f096c07d34e9a63639560a717130e7a87cbc82b7e3f/python_json_logger-4.1.0-py3-none-any.whl", hash = "sha256:132994765cf75bf44554be9aa49b06ef2345d23661a96720262716438141b6b2", size = 15021, upload-time = "2026-03-29T04:39:55.266Z" }, -] - -[[package]] -name = "pywinpty" -version = "3.0.5" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" } -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a9/ef/2d27f30c59a67be7025b2d7858c8c2d282b74d66544b2384730b82de74fd/pywinpty-3.0.5.tar.gz", hash = "sha256:61db0db063de9865adbea66db294628f8577f608d9764a4c7d3384eeacc4e81b", size = 16223484, upload-time = "2026-06-11T00:11:58.93Z" } -wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/45/34/942cc95ca4e26489875aa8a95192766247a687379ec29543eebe73ec945f/pywinpty-3.0.5-cp312-cp312-win_amd64.whl", hash = "sha256:d62946adf14b15b54c0b8d785f93fe18b04da23f4ad59e2e8c4612646e9abd23", size = 2090915, upload-time = "2026-06-10T23:43:14.98Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/6a/70/5b9053004844139ea8bd86209c57ade12b134b2782f383a095784c8531ec/pywinpty-3.0.5-cp312-cp312-win_arm64.whl", hash = "sha256:e9391c05fbfa7a992a97e831fc6849887b4014a614192e3d984a7ca59592b376", size = 815934, upload-time = "2026-06-10T23:41:42.384Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b9/f4/2a464b9893cceb3b3f416356e94fdc3e1bca9476993927e4e6d99fe95382/pywinpty-3.0.5-cp313-cp313-win_amd64.whl", hash = "sha256:48db1b0ad9d0a1b81dcaaa7163a99a7808deaceb0c1b2344716dc1fc090c3c4c", size = 2090471, upload-time = "2026-06-10T23:42:11.071Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/3c/2c/a138491a0afbdb50eb79395577bd326d4b0fbde7209417d1a8087ff2493a/pywinpty-3.0.5-cp313-cp313-win_arm64.whl", hash = "sha256:2c6008fb2d3774b48693b2fcb7f2cc317ade9dc581289a964ffeeaf81307c9b5", size = 815518, upload-time = "2026-06-10T23:42:02.363Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/6f/15/54400049a380582acd1282665c70fcf11e0bd3713679aca78e24c3aae738/pywinpty-3.0.5-cp313-cp313t-win_amd64.whl", hash = "sha256:22ce1b780d89821cc52daf6eac0708af22d93d000ce9c7c07e37489db8594598", size = 2089920, upload-time = "2026-06-10T23:44:13.395Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/94/0c/6f24f3c0799f502259b24bdf841a99ad2b0d59df5c2525b4e2a286d14be2/pywinpty-3.0.5-cp313-cp313t-win_arm64.whl", hash = "sha256:9c2919a81bc5cfb09b86fc5a002112b2de95ca4304a07413cbeeb746a1307a5c", size = 814520, upload-time = "2026-06-10T23:43:28.588Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e9/23/f3cd1b1e5fc56517f54452c49f92049e7dd9ffc8a63de22a495581f50d04/pywinpty-3.0.5-cp314-cp314-win_amd64.whl", hash = "sha256:03bb3c16d691d9242267201830bcd0e64a9b663170e9042bc84b210da9de15ac", size = 2090663, upload-time = "2026-06-10T23:43:59.845Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/9d/dd/96d6cbfc6d9ddab5c1c2f92c26545ae8997446a2ba7ee2024cd43c81f49b/pywinpty-3.0.5-cp314-cp314-win_arm64.whl", hash = "sha256:89c5c6ef08997a3b4b277b214a35fe15cab4dd6d119f0140aa71df5b1168fdbc", size = 815700, upload-time = "2026-06-10T23:40:50.001Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/30/36/d98087bce0acaa4cce7f196103cfa7be3f63ce65f52473bb3e38784ae5d9/pywinpty-3.0.5-cp314-cp314t-win_amd64.whl", hash = "sha256:7b566165e0c5fdd6abe167a5ac8b954be6a843eb55a85946576d6bc1dea03d6d", size = 2090093, upload-time = "2026-06-10T23:40:58.933Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/57/fd/fe2b0db922ba052ce3976a08f3fc05d0c05047c8b4ebb6102e832b8ef563/pywinpty-3.0.5-cp314-cp314t-win_arm64.whl", hash = "sha256:24366280a8aa677323da87bec729cb3ea3b35367386cece0978bdc6e4695c690", size = 814517, upload-time = "2026-06-10T23:42:34.946Z" }, + { 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]] name = "pyyaml" version = "6.0.3" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" } -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, ] [[package]] name = "pyzmq" version = "27.1.0" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" } +source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "implementation_name == 'pypy'" }, ] -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/04/0b/3c9baedbdf613ecaa7aa07027780b8867f57b6293b6ee50de316c9f3222b/pyzmq-27.1.0.tar.gz", hash = "sha256:ac0765e3d44455adb6ddbf4417dcce460fc40a05978c08efdf2948072f6db540", size = 281750, upload-time = "2025-09-08T23:10:18.157Z" } +sdist = { url = "https://files.pythonhosted.org/packages/04/0b/3c9baedbdf613ecaa7aa07027780b8867f57b6293b6ee50de316c9f3222b/pyzmq-27.1.0.tar.gz", hash = "sha256:ac0765e3d44455adb6ddbf4417dcce460fc40a05978c08efdf2948072f6db540", size = 281750, upload-time = "2025-09-08T23:10:18.157Z" } wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/92/e7/038aab64a946d535901103da16b953c8c9cc9c961dadcbf3609ed6428d23/pyzmq-27.1.0-cp312-abi3-macosx_10_15_universal2.whl", hash = "sha256:452631b640340c928fa343801b0d07eb0c3789a5ffa843f6e1a9cee0ba4eb4fc", size = 1306279, upload-time = "2025-09-08T23:08:03.807Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e8/5e/c3c49fdd0f535ef45eefcc16934648e9e59dace4a37ee88fc53f6cd8e641/pyzmq-27.1.0-cp312-abi3-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1c179799b118e554b66da67d88ed66cd37a169f1f23b5d9f0a231b4e8d44a113", size = 895645, upload-time = "2025-09-08T23:08:05.301Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f8/e5/b0b2504cb4e903a74dcf1ebae157f9e20ebb6ea76095f6cfffea28c42ecd/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3837439b7f99e60312f0c926a6ad437b067356dc2bc2ec96eb395fd0fe804233", size = 652574, upload-time = "2025-09-08T23:08:06.828Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f8/9b/c108cdb55560eaf253f0cbdb61b29971e9fb34d9c3499b0e96e4e60ed8a5/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43ad9a73e3da1fab5b0e7e13402f0b2fb934ae1c876c51d0afff0e7c052eca31", size = 840995, upload-time = "2025-09-08T23:08:08.396Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c2/bb/b79798ca177b9eb0825b4c9998c6af8cd2a7f15a6a1a4272c1d1a21d382f/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0de3028d69d4cdc475bfe47a6128eb38d8bc0e8f4d69646adfbcd840facbac28", size = 1642070, upload-time = "2025-09-08T23:08:09.989Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/9c/80/2df2e7977c4ede24c79ae39dcef3899bfc5f34d1ca7a5b24f182c9b7a9ca/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_i686.whl", hash = "sha256:cf44a7763aea9298c0aa7dbf859f87ed7012de8bda0f3977b6fb1d96745df856", size = 2021121, upload-time = "2025-09-08T23:08:11.907Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/46/bd/2d45ad24f5f5ae7e8d01525eb76786fa7557136555cac7d929880519e33a/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f30f395a9e6fbca195400ce833c731e7b64c3919aa481af4d88c3759e0cb7496", size = 1878550, upload-time = "2025-09-08T23:08:13.513Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e6/2f/104c0a3c778d7c2ab8190e9db4f62f0b6957b53c9d87db77c284b69f33ea/pyzmq-27.1.0-cp312-abi3-win32.whl", hash = "sha256:250e5436a4ba13885494412b3da5d518cd0d3a278a1ae640e113c073a5f88edd", size = 559184, upload-time = "2025-09-08T23:08:15.163Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/fc/7f/a21b20d577e4100c6a41795842028235998a643b1ad406a6d4163ea8f53e/pyzmq-27.1.0-cp312-abi3-win_amd64.whl", hash = "sha256:9ce490cf1d2ca2ad84733aa1d69ce6855372cb5ce9223802450c9b2a7cba0ccf", size = 619480, upload-time = "2025-09-08T23:08:17.192Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/78/c2/c012beae5f76b72f007a9e91ee9401cb88c51d0f83c6257a03e785c81cc2/pyzmq-27.1.0-cp312-abi3-win_arm64.whl", hash = "sha256:75a2f36223f0d535a0c919e23615fc85a1e23b71f40c7eb43d7b1dedb4d8f15f", size = 552993, upload-time = "2025-09-08T23:08:18.926Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/60/cb/84a13459c51da6cec1b7b1dc1a47e6db6da50b77ad7fd9c145842750a011/pyzmq-27.1.0-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:93ad4b0855a664229559e45c8d23797ceac03183c7b6f5b4428152a6b06684a5", size = 1122436, upload-time = "2025-09-08T23:08:20.801Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/dc/b6/94414759a69a26c3dd674570a81813c46a078767d931a6c70ad29fc585cb/pyzmq-27.1.0-cp313-cp313-android_24_x86_64.whl", hash = "sha256:fbb4f2400bfda24f12f009cba62ad5734148569ff4949b1b6ec3b519444342e6", size = 1156301, upload-time = "2025-09-08T23:08:22.47Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a5/ad/15906493fd40c316377fd8a8f6b1f93104f97a752667763c9b9c1b71d42d/pyzmq-27.1.0-cp313-cp313t-macosx_10_15_universal2.whl", hash = "sha256:e343d067f7b151cfe4eb3bb796a7752c9d369eed007b91231e817071d2c2fec7", size = 1341197, upload-time = "2025-09-08T23:08:24.286Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/14/1d/d343f3ce13db53a54cb8946594e567410b2125394dafcc0268d8dda027e0/pyzmq-27.1.0-cp313-cp313t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:08363b2011dec81c354d694bdecaef4770e0ae96b9afea70b3f47b973655cc05", size = 897275, upload-time = "2025-09-08T23:08:26.063Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/69/2d/d83dd6d7ca929a2fc67d2c3005415cdf322af7751d773524809f9e585129/pyzmq-27.1.0-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d54530c8c8b5b8ddb3318f481297441af102517602b569146185fa10b63f4fa9", size = 660469, upload-time = "2025-09-08T23:08:27.623Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/3e/cd/9822a7af117f4bc0f1952dbe9ef8358eb50a24928efd5edf54210b850259/pyzmq-27.1.0-cp313-cp313t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f3afa12c392f0a44a2414056d730eebc33ec0926aae92b5ad5cf26ebb6cc128", size = 847961, upload-time = "2025-09-08T23:08:29.672Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/9a/12/f003e824a19ed73be15542f172fd0ec4ad0b60cf37436652c93b9df7c585/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c65047adafe573ff023b3187bb93faa583151627bc9c51fc4fb2c561ed689d39", size = 1650282, upload-time = "2025-09-08T23:08:31.349Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d5/4a/e82d788ed58e9a23995cee70dbc20c9aded3d13a92d30d57ec2291f1e8a3/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:90e6e9441c946a8b0a667356f7078d96411391a3b8f80980315455574177ec97", size = 2024468, upload-time = "2025-09-08T23:08:33.543Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d9/94/2da0a60841f757481e402b34bf4c8bf57fa54a5466b965de791b1e6f747d/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:add071b2d25f84e8189aaf0882d39a285b42fa3853016ebab234a5e78c7a43db", size = 1885394, upload-time = "2025-09-08T23:08:35.51Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/4f/6f/55c10e2e49ad52d080dc24e37adb215e5b0d64990b57598abc2e3f01725b/pyzmq-27.1.0-cp313-cp313t-win32.whl", hash = "sha256:7ccc0700cfdf7bd487bea8d850ec38f204478681ea02a582a8da8171b7f90a1c", size = 574964, upload-time = "2025-09-08T23:08:37.178Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/87/4d/2534970ba63dd7c522d8ca80fb92777f362c0f321900667c615e2067cb29/pyzmq-27.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:8085a9fba668216b9b4323be338ee5437a235fe275b9d1610e422ccc279733e2", size = 641029, upload-time = "2025-09-08T23:08:40.595Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f6/fa/f8aea7a28b0641f31d40dea42d7ef003fded31e184ef47db696bc74cd610/pyzmq-27.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:6bb54ca21bcfe361e445256c15eedf083f153811c37be87e0514934d6913061e", size = 561541, upload-time = "2025-09-08T23:08:42.668Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/87/45/19efbb3000956e82d0331bafca5d9ac19ea2857722fa2caacefb6042f39d/pyzmq-27.1.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:ce980af330231615756acd5154f29813d553ea555485ae712c491cd483df6b7a", size = 1341197, upload-time = "2025-09-08T23:08:44.973Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/48/43/d72ccdbf0d73d1343936296665826350cb1e825f92f2db9db3e61c2162a2/pyzmq-27.1.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1779be8c549e54a1c38f805e56d2a2e5c009d26de10921d7d51cfd1c8d4632ea", size = 897175, upload-time = "2025-09-08T23:08:46.601Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/2f/2e/a483f73a10b65a9ef0161e817321d39a770b2acf8bcf3004a28d90d14a94/pyzmq-27.1.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7200bb0f03345515df50d99d3db206a0a6bee1955fbb8c453c76f5bf0e08fb96", size = 660427, upload-time = "2025-09-08T23:08:48.187Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f5/d2/5f36552c2d3e5685abe60dfa56f91169f7a2d99bbaf67c5271022ab40863/pyzmq-27.1.0-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01c0e07d558b06a60773744ea6251f769cd79a41a97d11b8bf4ab8f034b0424d", size = 847929, upload-time = "2025-09-08T23:08:49.76Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c4/2a/404b331f2b7bf3198e9945f75c4c521f0c6a3a23b51f7a4a401b94a13833/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:80d834abee71f65253c91540445d37c4c561e293ba6e741b992f20a105d69146", size = 1650193, upload-time = "2025-09-08T23:08:51.7Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/1c/0b/f4107e33f62a5acf60e3ded67ed33d79b4ce18de432625ce2fc5093d6388/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:544b4e3b7198dde4a62b8ff6685e9802a9a1ebf47e77478a5eb88eca2a82f2fd", size = 2024388, upload-time = "2025-09-08T23:08:53.393Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/0d/01/add31fe76512642fd6e40e3a3bd21f4b47e242c8ba33efb6809e37076d9b/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cedc4c68178e59a4046f97eca31b148ddcf51e88677de1ef4e78cf06c5376c9a", size = 1885316, upload-time = "2025-09-08T23:08:55.702Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c4/59/a5f38970f9bf07cee96128de79590bb354917914a9be11272cfc7ff26af0/pyzmq-27.1.0-cp314-cp314t-win32.whl", hash = "sha256:1f0b2a577fd770aa6f053211a55d1c47901f4d537389a034c690291485e5fe92", size = 587472, upload-time = "2025-09-08T23:08:58.18Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/70/d8/78b1bad170f93fcf5e3536e70e8fadac55030002275c9a29e8f5719185de/pyzmq-27.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:19c9468ae0437f8074af379e986c5d3d7d7bfe033506af442e8c879732bedbe0", size = 661401, upload-time = "2025-09-08T23:08:59.802Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/81/d6/4bfbb40c9a0b42fc53c7cf442f6385db70b40f74a783130c5d0a5aa62228/pyzmq-27.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dc5dbf68a7857b59473f7df42650c621d7e8923fb03fa74a526890f4d33cc4d7", size = 575170, upload-time = "2025-09-08T23:09:01.418Z" }, + { url = "https://files.pythonhosted.org/packages/92/e7/038aab64a946d535901103da16b953c8c9cc9c961dadcbf3609ed6428d23/pyzmq-27.1.0-cp312-abi3-macosx_10_15_universal2.whl", hash = "sha256:452631b640340c928fa343801b0d07eb0c3789a5ffa843f6e1a9cee0ba4eb4fc", size = 1306279, upload-time = "2025-09-08T23:08:03.807Z" }, + { url = "https://files.pythonhosted.org/packages/e8/5e/c3c49fdd0f535ef45eefcc16934648e9e59dace4a37ee88fc53f6cd8e641/pyzmq-27.1.0-cp312-abi3-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1c179799b118e554b66da67d88ed66cd37a169f1f23b5d9f0a231b4e8d44a113", size = 895645, upload-time = "2025-09-08T23:08:05.301Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e5/b0b2504cb4e903a74dcf1ebae157f9e20ebb6ea76095f6cfffea28c42ecd/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3837439b7f99e60312f0c926a6ad437b067356dc2bc2ec96eb395fd0fe804233", size = 652574, upload-time = "2025-09-08T23:08:06.828Z" }, + { url = "https://files.pythonhosted.org/packages/f8/9b/c108cdb55560eaf253f0cbdb61b29971e9fb34d9c3499b0e96e4e60ed8a5/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43ad9a73e3da1fab5b0e7e13402f0b2fb934ae1c876c51d0afff0e7c052eca31", size = 840995, upload-time = "2025-09-08T23:08:08.396Z" }, + { url = "https://files.pythonhosted.org/packages/c2/bb/b79798ca177b9eb0825b4c9998c6af8cd2a7f15a6a1a4272c1d1a21d382f/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0de3028d69d4cdc475bfe47a6128eb38d8bc0e8f4d69646adfbcd840facbac28", size = 1642070, upload-time = "2025-09-08T23:08:09.989Z" }, + { url = "https://files.pythonhosted.org/packages/9c/80/2df2e7977c4ede24c79ae39dcef3899bfc5f34d1ca7a5b24f182c9b7a9ca/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_i686.whl", hash = "sha256:cf44a7763aea9298c0aa7dbf859f87ed7012de8bda0f3977b6fb1d96745df856", size = 2021121, upload-time = "2025-09-08T23:08:11.907Z" }, + { url = "https://files.pythonhosted.org/packages/46/bd/2d45ad24f5f5ae7e8d01525eb76786fa7557136555cac7d929880519e33a/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f30f395a9e6fbca195400ce833c731e7b64c3919aa481af4d88c3759e0cb7496", size = 1878550, upload-time = "2025-09-08T23:08:13.513Z" }, + { url = "https://files.pythonhosted.org/packages/e6/2f/104c0a3c778d7c2ab8190e9db4f62f0b6957b53c9d87db77c284b69f33ea/pyzmq-27.1.0-cp312-abi3-win32.whl", hash = "sha256:250e5436a4ba13885494412b3da5d518cd0d3a278a1ae640e113c073a5f88edd", size = 559184, upload-time = "2025-09-08T23:08:15.163Z" }, + { url = "https://files.pythonhosted.org/packages/fc/7f/a21b20d577e4100c6a41795842028235998a643b1ad406a6d4163ea8f53e/pyzmq-27.1.0-cp312-abi3-win_amd64.whl", hash = "sha256:9ce490cf1d2ca2ad84733aa1d69ce6855372cb5ce9223802450c9b2a7cba0ccf", size = 619480, upload-time = "2025-09-08T23:08:17.192Z" }, + { url = "https://files.pythonhosted.org/packages/78/c2/c012beae5f76b72f007a9e91ee9401cb88c51d0f83c6257a03e785c81cc2/pyzmq-27.1.0-cp312-abi3-win_arm64.whl", hash = "sha256:75a2f36223f0d535a0c919e23615fc85a1e23b71f40c7eb43d7b1dedb4d8f15f", size = 552993, upload-time = "2025-09-08T23:08:18.926Z" }, + { url = "https://files.pythonhosted.org/packages/60/cb/84a13459c51da6cec1b7b1dc1a47e6db6da50b77ad7fd9c145842750a011/pyzmq-27.1.0-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:93ad4b0855a664229559e45c8d23797ceac03183c7b6f5b4428152a6b06684a5", size = 1122436, upload-time = "2025-09-08T23:08:20.801Z" }, + { url = "https://files.pythonhosted.org/packages/dc/b6/94414759a69a26c3dd674570a81813c46a078767d931a6c70ad29fc585cb/pyzmq-27.1.0-cp313-cp313-android_24_x86_64.whl", hash = "sha256:fbb4f2400bfda24f12f009cba62ad5734148569ff4949b1b6ec3b519444342e6", size = 1156301, upload-time = "2025-09-08T23:08:22.47Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ad/15906493fd40c316377fd8a8f6b1f93104f97a752667763c9b9c1b71d42d/pyzmq-27.1.0-cp313-cp313t-macosx_10_15_universal2.whl", hash = "sha256:e343d067f7b151cfe4eb3bb796a7752c9d369eed007b91231e817071d2c2fec7", size = 1341197, upload-time = "2025-09-08T23:08:24.286Z" }, + { url = "https://files.pythonhosted.org/packages/14/1d/d343f3ce13db53a54cb8946594e567410b2125394dafcc0268d8dda027e0/pyzmq-27.1.0-cp313-cp313t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:08363b2011dec81c354d694bdecaef4770e0ae96b9afea70b3f47b973655cc05", size = 897275, upload-time = "2025-09-08T23:08:26.063Z" }, + { url = "https://files.pythonhosted.org/packages/69/2d/d83dd6d7ca929a2fc67d2c3005415cdf322af7751d773524809f9e585129/pyzmq-27.1.0-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d54530c8c8b5b8ddb3318f481297441af102517602b569146185fa10b63f4fa9", size = 660469, upload-time = "2025-09-08T23:08:27.623Z" }, + { url = "https://files.pythonhosted.org/packages/3e/cd/9822a7af117f4bc0f1952dbe9ef8358eb50a24928efd5edf54210b850259/pyzmq-27.1.0-cp313-cp313t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f3afa12c392f0a44a2414056d730eebc33ec0926aae92b5ad5cf26ebb6cc128", size = 847961, upload-time = "2025-09-08T23:08:29.672Z" }, + { url = "https://files.pythonhosted.org/packages/9a/12/f003e824a19ed73be15542f172fd0ec4ad0b60cf37436652c93b9df7c585/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c65047adafe573ff023b3187bb93faa583151627bc9c51fc4fb2c561ed689d39", size = 1650282, upload-time = "2025-09-08T23:08:31.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/4a/e82d788ed58e9a23995cee70dbc20c9aded3d13a92d30d57ec2291f1e8a3/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:90e6e9441c946a8b0a667356f7078d96411391a3b8f80980315455574177ec97", size = 2024468, upload-time = "2025-09-08T23:08:33.543Z" }, + { url = "https://files.pythonhosted.org/packages/d9/94/2da0a60841f757481e402b34bf4c8bf57fa54a5466b965de791b1e6f747d/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:add071b2d25f84e8189aaf0882d39a285b42fa3853016ebab234a5e78c7a43db", size = 1885394, upload-time = "2025-09-08T23:08:35.51Z" }, + { url = "https://files.pythonhosted.org/packages/4f/6f/55c10e2e49ad52d080dc24e37adb215e5b0d64990b57598abc2e3f01725b/pyzmq-27.1.0-cp313-cp313t-win32.whl", hash = "sha256:7ccc0700cfdf7bd487bea8d850ec38f204478681ea02a582a8da8171b7f90a1c", size = 574964, upload-time = "2025-09-08T23:08:37.178Z" }, + { url = "https://files.pythonhosted.org/packages/87/4d/2534970ba63dd7c522d8ca80fb92777f362c0f321900667c615e2067cb29/pyzmq-27.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:8085a9fba668216b9b4323be338ee5437a235fe275b9d1610e422ccc279733e2", size = 641029, upload-time = "2025-09-08T23:08:40.595Z" }, + { url = "https://files.pythonhosted.org/packages/f6/fa/f8aea7a28b0641f31d40dea42d7ef003fded31e184ef47db696bc74cd610/pyzmq-27.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:6bb54ca21bcfe361e445256c15eedf083f153811c37be87e0514934d6913061e", size = 561541, upload-time = "2025-09-08T23:08:42.668Z" }, + { url = "https://files.pythonhosted.org/packages/87/45/19efbb3000956e82d0331bafca5d9ac19ea2857722fa2caacefb6042f39d/pyzmq-27.1.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:ce980af330231615756acd5154f29813d553ea555485ae712c491cd483df6b7a", size = 1341197, upload-time = "2025-09-08T23:08:44.973Z" }, + { url = "https://files.pythonhosted.org/packages/48/43/d72ccdbf0d73d1343936296665826350cb1e825f92f2db9db3e61c2162a2/pyzmq-27.1.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1779be8c549e54a1c38f805e56d2a2e5c009d26de10921d7d51cfd1c8d4632ea", size = 897175, upload-time = "2025-09-08T23:08:46.601Z" }, + { url = "https://files.pythonhosted.org/packages/2f/2e/a483f73a10b65a9ef0161e817321d39a770b2acf8bcf3004a28d90d14a94/pyzmq-27.1.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7200bb0f03345515df50d99d3db206a0a6bee1955fbb8c453c76f5bf0e08fb96", size = 660427, upload-time = "2025-09-08T23:08:48.187Z" }, + { url = "https://files.pythonhosted.org/packages/f5/d2/5f36552c2d3e5685abe60dfa56f91169f7a2d99bbaf67c5271022ab40863/pyzmq-27.1.0-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01c0e07d558b06a60773744ea6251f769cd79a41a97d11b8bf4ab8f034b0424d", size = 847929, upload-time = "2025-09-08T23:08:49.76Z" }, + { url = "https://files.pythonhosted.org/packages/c4/2a/404b331f2b7bf3198e9945f75c4c521f0c6a3a23b51f7a4a401b94a13833/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:80d834abee71f65253c91540445d37c4c561e293ba6e741b992f20a105d69146", size = 1650193, upload-time = "2025-09-08T23:08:51.7Z" }, + { url = "https://files.pythonhosted.org/packages/1c/0b/f4107e33f62a5acf60e3ded67ed33d79b4ce18de432625ce2fc5093d6388/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:544b4e3b7198dde4a62b8ff6685e9802a9a1ebf47e77478a5eb88eca2a82f2fd", size = 2024388, upload-time = "2025-09-08T23:08:53.393Z" }, + { url = "https://files.pythonhosted.org/packages/0d/01/add31fe76512642fd6e40e3a3bd21f4b47e242c8ba33efb6809e37076d9b/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cedc4c68178e59a4046f97eca31b148ddcf51e88677de1ef4e78cf06c5376c9a", size = 1885316, upload-time = "2025-09-08T23:08:55.702Z" }, + { url = "https://files.pythonhosted.org/packages/c4/59/a5f38970f9bf07cee96128de79590bb354917914a9be11272cfc7ff26af0/pyzmq-27.1.0-cp314-cp314t-win32.whl", hash = "sha256:1f0b2a577fd770aa6f053211a55d1c47901f4d537389a034c690291485e5fe92", size = 587472, upload-time = "2025-09-08T23:08:58.18Z" }, + { url = "https://files.pythonhosted.org/packages/70/d8/78b1bad170f93fcf5e3536e70e8fadac55030002275c9a29e8f5719185de/pyzmq-27.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:19c9468ae0437f8074af379e986c5d3d7d7bfe033506af442e8c879732bedbe0", size = 661401, upload-time = "2025-09-08T23:08:59.802Z" }, + { url = "https://files.pythonhosted.org/packages/81/d6/4bfbb40c9a0b42fc53c7cf442f6385db70b40f74a783130c5d0a5aa62228/pyzmq-27.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dc5dbf68a7857b59473f7df42650c621d7e8923fb03fa74a526890f4d33cc4d7", size = 575170, upload-time = "2025-09-08T23:09:01.418Z" }, +] + +[[package]] +name = "redis" +version = "5.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/47/da/d283a37303a995cd36f8b92db85135153dc4f7a8e4441aa827721b442cfb/redis-5.2.1.tar.gz", hash = "sha256:16f2e22dff21d5125e8481515e386711a34cbec50f0e44413dd7d9c060a54e0f", size = 4608355, upload-time = "2024-12-06T09:50:41.956Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/5f/fa26b9b2672cbe30e07d9a5bdf39cf16e3b80b42916757c5f92bca88e4ba/redis-5.2.1-py3-none-any.whl", hash = "sha256:ee7e1056b9aea0f04c6c2ed59452947f34c4940ee025f5dd83e6a6418b6989e4", size = 261502, upload-time = "2024-12-06T09:50:39.656Z" }, ] [[package]] name = "referencing" version = "0.37.0" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" } +source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, { name = "rpds-py" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } +sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, -] - -[[package]] -name = "requests" -version = "2.34.2" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" } -dependencies = [ - { name = "certifi" }, - { name = "charset-normalizer" }, - { name = "idna" }, - { name = "urllib3" }, -] -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" } -wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, -] - -[[package]] -name = "rfc3339-validator" -version = "0.1.4" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" } -dependencies = [ - { name = "six" }, -] -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/28/ea/a9387748e2d111c3c2b275ba970b735e04e15cdb1eb30693b6b5708c4dbd/rfc3339_validator-0.1.4.tar.gz", hash = "sha256:138a2abdf93304ad60530167e51d2dfb9549521a836871b88d7f4695d0022f6b", size = 5513, upload-time = "2021-05-12T16:37:54.178Z" } -wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/7b/44/4e421b96b67b2daff264473f7465db72fbdf36a07e05494f50300cc7b0c6/rfc3339_validator-0.1.4-py2.py3-none-any.whl", hash = "sha256:24f6ec1eda14ef823da9e36ec7113124b39c04d50a4d3d3a3c2859577e7791fa", size = 3490, upload-time = "2021-05-12T16:37:52.536Z" }, -] - -[[package]] -name = "rfc3986-validator" -version = "0.1.1" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" } -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/da/88/f270de456dd7d11dcc808abfa291ecdd3f45ff44e3b549ffa01b126464d0/rfc3986_validator-0.1.1.tar.gz", hash = "sha256:3d44bde7921b3b9ec3ae4e3adca370438eccebc676456449b145d533b240d055", size = 6760, upload-time = "2019-10-28T16:00:19.144Z" } -wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/9e/51/17023c0f8f1869d8806b979a2bffa3f861f26a3f1a66b094288323fba52f/rfc3986_validator-0.1.1-py2.py3-none-any.whl", hash = "sha256:2f235c432ef459970b4306369336b9d5dbdda31b510ca1e327636e01f528bfa9", size = 4242, upload-time = "2019-10-28T16:00:13.976Z" }, -] - -[[package]] -name = "rfc3987-syntax" -version = "1.1.0" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" } -dependencies = [ - { name = "lark" }, -] -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/2c/06/37c1a5557acf449e8e406a830a05bf885ac47d33270aec454ef78675008d/rfc3987_syntax-1.1.0.tar.gz", hash = "sha256:717a62cbf33cffdd16dfa3a497d81ce48a660ea691b1ddd7be710c22f00b4a0d", size = 14239, upload-time = "2025-07-18T01:05:05.015Z" } -wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/7e/71/44ce230e1b7fadd372515a97e32a83011f906ddded8d03e3c6aafbdedbb7/rfc3987_syntax-1.1.0-py3-none-any.whl", hash = "sha256:6c3d97604e4c5ce9f714898e05401a0445a641cfa276432b0a648c80856f6a3f", size = 8046, upload-time = "2025-07-18T01:05:03.843Z" }, + { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, ] [[package]] name = "rpds-py" version = "2026.6.3" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" } -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/aa/2a/9618a122aeb2a169a28b03889a2995fe297588964333d4a7d67bdf46e147/rpds_py-2026.6.3.tar.gz", hash = "sha256:1cebd1337c242e4ec2293e541f712b2da849b29f48f0c293684b71c0632625d4", size = 64051, upload-time = "2026-06-30T07:17:53.009Z" } +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/2a/9618a122aeb2a169a28b03889a2995fe297588964333d4a7d67bdf46e147/rpds_py-2026.6.3.tar.gz", hash = "sha256:1cebd1337c242e4ec2293e541f712b2da849b29f48f0c293684b71c0632625d4", size = 64051, upload-time = "2026-06-30T07:17:53.009Z" } wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/5c/be/2e8974163072e7bab7df1a5acd54c4498e75e35d6d18b864d3a9d5dadc92/rpds_py-2026.6.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a0811d33247c3d6128a3001d763f2aa056bb3425204335400ac54f89eec3a0d0", size = 343691, upload-time = "2026-06-30T07:15:14.96Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a4/73/319dfa745dd668efe89309141ded489126461fcecd2b8f3a3cda185129b6/rpds_py-2026.6.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:538949e262e46caa31ac01bdb3c1e8f642622922cacbabbae6a8445d9dc33eaf", size = 338542, upload-time = "2026-06-30T07:15:16.267Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/21/63/4239893be1c4d09b709b1a8f6be4188f0870084ff547f46606b8a75f1b03/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55927d532399c2c646100ff7feb48eaa940ad70f42cd68e1328f3ded9f81ca24", size = 368180, upload-time = "2026-06-30T07:15:17.62Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/1c/ca/9c5de382225234ceb37b1844ebdb140db12b2a278bb9efe2fcd19f6c82ce/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f56f1695bc5c0871cbc33dc0130fcf503aab0c57dcc5a6700a4f49eba4f2652e", size = 375067, upload-time = "2026-06-30T07:15:18.952Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/87/dc/863f69d1bf04ade34b7fe0d59b9fdf6f0135fe2d7cbca74f1d665589559d/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:270b293dae9058fc9fcedab50f13cebf46fb8ed1d1d54e0521a9da5d6b211975", size = 490509, upload-time = "2026-06-30T07:15:20.434Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ce/ef/eac16a12048b45ec7c7fa94f2be3438a5f26bf9cc8580b18a1cfd609b7f6/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:127565fead0a10943b282957bd5447804ff3160ad79f2ad2635e6d249e380680", size = 382754, upload-time = "2026-06-30T07:15:21.831Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/04/8f/d2f3f532616be4d06c316ef119683e832bd3d41e112bf3a88f4151c95b17/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ecabd69db66de867690f9797f2f8fa27ba501bbc24540cbdbdc649cd15888ba6", size = 366189, upload-time = "2026-06-30T07:15:23.371Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e3/29/41a7b0e98a4b44cd676ab7598419623373eb43b20be68c084935c1a8cf88/rpds_py-2026.6.3-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:58eadac9cd119677b60e1cf8ac4052f35949d71b8a9e5556efccbe82533cf22a", size = 377750, upload-time = "2026-06-30T07:15:24.659Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/2e/05/ecda0bec46f9a1565090bcdc941d023f6a25aff85fda28f89f8d19878152/rpds_py-2026.6.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7491ee23305ac3eb59e492b6945881f5cd77a6f731061a3f25b77fd40f9e99a4", size = 395576, upload-time = "2026-06-30T07:15:25.987Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/68/a8/6ed52f03ee6cb854ce78785cc9a9a672eb880e83fd7224d471f667d151f1/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2c99f7e8ccb3dd6e3e4bfeac657a7b208c9bac8075f4b078c02d7404c34107fa", size = 543807, upload-time = "2026-06-30T07:15:27.356Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/8f/d6/156c0d3eea27ba09b92562ba2364ba124c0a061b199e17eac637cd25a5e2/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:62698275682bf121181861295c9181e789030a2d516071f5b8f3c23c170cd0fc", size = 611187, upload-time = "2026-06-30T07:15:28.931Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f1/31/774212ed989c62f7f310220089f9b0a3fb8f40f5443d1727abd5d9f52bc9/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a214c993455f99a89aaeadc9b21241900037adc9d97203e374d75513c5911822", size = 573030, upload-time = "2026-06-30T07:15:30.553Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c9/50/22f73127a41f1ce4f87fe39aadfb9a126345801c274aa93ae88456249327/rpds_py-2026.6.3-cp312-cp312-win32.whl", hash = "sha256:501f9f04a588d6a09179368c57071301445191767c64e4b52a6aa9871f1ef5ed", size = 202185, upload-time = "2026-06-30T07:15:32.027Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/04/3a/f0ee4d4dde9d3b69dedf1b5f74e7a40017046d55052d173e418c6a94f960/rpds_py-2026.6.3-cp312-cp312-win_amd64.whl", hash = "sha256:2c958bf94822e9290a40aaf2a822d4bc5c88099093e3948ad6c571eca9272e5f", size = 220394, upload-time = "2026-06-30T07:15:33.359Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f3/83/3382fe37f809b59f02aac04dbc4e765b480b46ee0227ed516e3bdc4d3dfc/rpds_py-2026.6.3-cp312-cp312-win_arm64.whl", hash = "sha256:22bffe6042b9bcb0822bcd1955ec00e245daf17b4344e4ed8e9551b976b63e96", size = 215753, upload-time = "2026-06-30T07:15:34.778Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a4/9e/b818ee580026ec578138e961027a68820c40afeb1ec8f6819b54fb99e196/rpds_py-2026.6.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:3cfe765c1da0072636ca06628261e0ea05688e160d5c8a03e0217c3854037223", size = 343012, upload-time = "2026-06-30T07:15:36.005Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f3/6b/686d9dc4359a8f163cfbbf89ee0b4e586431de22fe8248edb63a8cf50d49/rpds_py-2026.6.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f4d78253f6996be4901669ad25319f842f740eccf4d58e3c7f3dd39e6dde1d8f", size = 338203, upload-time = "2026-06-30T07:15:37.462Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/9e/9b/069aa329940f8207615e091f5eedbbd40e1e15eac68a0790fd05ccdf796c/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54f45a148e28767bf343d33a684693c70e451c6f4c0e9904709a723fafbdfc1f", size = 367984, upload-time = "2026-06-30T07:15:39.008Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/14/db/34c203e4becff3703e4d3bc121842c00b8689197f398161203a880052f4e/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:842e7b070435622248c7a2c44ae53fa1440e073cc3023bc919fed570884097a7", size = 374815, upload-time = "2026-06-30T07:15:40.253Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ee/7d/8071067d2cc453d916ad836e828c943f575e8a44612537759002a1e07381/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8020133a74bd81b4572dd8e4be028a6b1ebcd70e6726edc3918008c08bee6ee6", size = 490545, upload-time = "2026-06-30T07:15:41.729Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a3/42/da06c5aa8f0484ff07f270787434204d9f4535e2f8c3b51ed402267e63c3/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cdc7e35386f3847df728fbcb5e887e2d79c19e2fa1eba9e51b6621d23e3243af", size = 382828, upload-time = "2026-06-30T07:15:43.327Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/57/d7/fe978efc2ae50abe48eb7464668ea99f53c010c60aeebb7b35ad27f23661/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acac386b453c2516111b50985d60ce46e7fadb5ea71ae7b25f4c946935bf27cf", size = 365678, upload-time = "2026-06-30T07:15:44.992Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/69/9d/1d8922e1990b2a6eb532b6ff53d3e73d2b3bbffc84116c75826bee73dfc6/rpds_py-2026.6.3-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:425560c6fa0415f27261727bb20bd097568485e5eb0c121f1949417d1c516885", size = 377811, upload-time = "2026-06-30T07:15:46.523Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b1/3d/198dceafb4fb034a6a47347e1b0735d34e0bd4a50be4e898d408ee66cb14/rpds_py-2026.6.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a550fb4950a06dde3beb4721f5ad4b25bf4513784665b0a8522c792e2bd822a4", size = 395382, upload-time = "2026-06-30T07:15:47.955Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/1f/f1/13968e49655d40b6b19d8b9140296bbc6f1d86b3f0f6c346cf9f1adddf4b/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4f4bca01b63096f606e095734dd56e74e175f94cfbf24ff3d63281cec61f7bb7", size = 543832, upload-time = "2026-06-30T07:15:49.33Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ac/ab/289bcb1b90bd3e40a2900c561fa0e2087345ecbb094f0b870f2345142b7c/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ccffae9a092a00deb7efd545fe5e2c33c33b88e7c054337e9a74c179347d0b7d", size = 611011, upload-time = "2026-06-30T07:15:50.847Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/1e/16/5043105e679436ccfbc8e5e0dd2d663ed18a8b8113515fd06a5e5d77c83e/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1cf01971c4f2c5553b772a542e4aaf191789cd331bc2cd4ff0e6e65ba49e1e97", size = 572431, upload-time = "2026-06-30T07:15:52.394Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/85/ed/adab103321c0a6565d5ae1c2998349bc3ee175b82ccc5ae8fc04cc413075/rpds_py-2026.6.3-cp313-cp313-win32.whl", hash = "sha256:8c3d1e9c15b9d51ca0391e13da1a25a0a4df3c58a37c9dc368e0736cf7f69df0", size = 201710, upload-time = "2026-06-30T07:15:53.894Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/7b/ed/a03b09668e74e5dabbf2e211f6468e1820c0552f7b0500082da31841bf7b/rpds_py-2026.6.3-cp313-cp313-win_amd64.whl", hash = "sha256:9250a9a0a6fd4648b3f868da8d91a4c52b5811a62df58e753d50ae4454a36f80", size = 219454, upload-time = "2026-06-30T07:15:55.25Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/27/17/b8642c12930b71bc2b25831f6708ccf0f75abcd11883932ec9ce54ba3a78/rpds_py-2026.6.3-cp313-cp313-win_arm64.whl", hash = "sha256:900a67df3fd1660b035a4761c4ce73c382ea6b35f90f9863c36c6fd8bf8b09bb", size = 215063, upload-time = "2026-06-30T07:15:56.573Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b6/36/7fbe9dcdaf857fb3f63c2a2284b62492d95f5e8334e947e5fb6e7f68c9be/rpds_py-2026.6.3-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:931908d9fc855d8f74783377822be318edb6dcb19e47169dc038f9a1bf60b06e", size = 344510, upload-time = "2026-06-30T07:15:57.921Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ba/54/f785cc3d3f60839ca57a5af4927a9f347b07b2799c373fc20f7949f87c7e/rpds_py-2026.6.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d7469697dce35be237db177d42e2a2ee26e6dcc5fc052078a6fefabd288c6edd", size = 339495, upload-time = "2026-06-30T07:15:59.238Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/63/ef/d4cdaf309e6b095b43597103cf8c0b951d6cca2acce68c474f75ec12e0c7/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bcfbcf66006befb9fd2aeaa9e01feaf881b4dc330a02ba07d2322b1c11be7b5d", size = 369454, upload-time = "2026-06-30T07:16:01.021Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/96/4a/9559a68b7ee15db09d7981212e8c2e219d2a1d6d4faa0391d813c3496a36/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:847927daf4cffbd4e90e42bc890069897101edd015f956cb8721b3473372edda", size = 374583, upload-time = "2026-06-30T07:16:02.287Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ef/75/8964aa7d2c6e8ac43eba8eb6e6b0fdda1f46d39f2fc3e6aa9f2cb17f485d/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aca6c1ef08a82bfe327cc156da694660f599923e2e6665b6d81c9c2d0ac9ffc8", size = 492919, upload-time = "2026-06-30T07:16:03.723Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/8f/97/6908094ac804115e65aedfd90f1b5fee4eebebd3f6c4cfc5419939267565/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ae50181a047c871561212bb97f7932a2d45fb53e947bd9b57ebad85b529cbc53", size = 383725, upload-time = "2026-06-30T07:16:05.305Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d1/9c/0d1fdc2e7aba23e290d603bc494e97bd205bae262ce33c6b32a69768ed5e/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc319e5a1de4b6913aac94bf6a2f9e847371e0a140a43dd4991db1a09bc2d504", size = 367255, upload-time = "2026-06-30T07:16:07.086Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c4/fe/f0209ca4a9ed074bc8acb44dfd0e81c3122e94c9689f5645b7973a866719/rpds_py-2026.6.3-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:e4316bf32babbed84e691e352faf967ce2f0f024174a8643c37c94a1080374fc", size = 379060, upload-time = "2026-06-30T07:16:08.525Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c6/8d/f1cc54c616b9d8897de8738aac148d20afca93f68187475fe194d09a71b9/rpds_py-2026.6.3-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8c6e5a2f750cc71c3e3b11d71661f21d6f9bc6cebc6564b1466417a1ec03ec77", size = 395960, upload-time = "2026-06-30T07:16:09.989Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/fb/04/aafff00f73aeca2945f734f1d483c64ab8f472d0864ab02377fd8e89c3b2/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4470ce197d4090875cf6affbf1f853338387428df97c4fb7b7106317b8214698", size = 545356, upload-time = "2026-06-30T07:16:11.816Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/fd/cc/e229663b9e4ddac5a4acbe9085dd80a71af2a5d356b8b39d6bff233f24b0/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ea964164cc9afa72d4d9b23cc28dafae93693c0a53e0b42acbff15b22c3f9ddd", size = 612319, upload-time = "2026-06-30T07:16:13.586Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e3/7a/8a0e6d3e6cd066af108b71b43122c3fe158dd9eb86acac626593a2582eb1/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:639c8929aa0afe81be836b04de888460d6bed38b9c54cfc18da8f6bfabf5af5d", size = 573508, upload-time = "2026-06-30T07:16:15.23Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/87/03/2a69ab618a789cf6cf85c86bb844c62d090e700ab1a2aa676b3741b6c516/rpds_py-2026.6.3-cp314-cp314-win32.whl", hash = "sha256:882076c00c0a608b131187055ddc5ae29f2e7eaf870d6168980420d58528a5c8", size = 202504, upload-time = "2026-06-30T07:16:16.893Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/85/62/a3892ba945f4e24c78f352e5de3c7620d8479f73f211406a97263d13c7d2/rpds_py-2026.6.3-cp314-cp314-win_amd64.whl", hash = "sha256:0be972be84cfcaf46c8c6edf690ca0f154ac17babf1f6a955a51579b34ad2dc5", size = 220380, upload-time = "2026-06-30T07:16:18.108Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/3d/e7/c2bd44dc831931815ad11ebb5f430b5a0a4d3caa9de837107876c30c3432/rpds_py-2026.6.3-cp314-cp314-win_arm64.whl", hash = "sha256:2a9c6f195058cb45335e8cc3802745c603d716eb96bc9625950c1aac71c0c703", size = 215976, upload-time = "2026-06-30T07:16:19.654Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/79/9c/fff7b74bce9a091ec9a012a03f9ff5f69364eaf9451060dfc4486da2ffdd/rpds_py-2026.6.3-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:f90938e92afda60266da758ee7d363447f7f0138c9559f9e1811629580582d90", size = 346840, upload-time = "2026-06-30T07:16:21.268Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e9/44/77bcb1168b33704908295533d27f10eb811e9e3e193e8993dc99572211d3/rpds_py-2026.6.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ec829541c45bca16e61c7ae50c20501f213605beb75d1aba91a6ee37fbbb56a4", size = 340282, upload-time = "2026-06-30T07:16:22.875Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/87/3c/7a9081c7c9e645b39efe19e4ffbeccd80add246327cd9b888aecffd72317/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afd70d95892096cdb26f15a00c45907b17817577aa8d1c76b2dcc2788391f9e9", size = 370403, upload-time = "2026-06-30T07:16:24.415Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f7/69/af47021eb7dad6ff3396cb001c08f0f3c4d06c20253f75be6421a59fe6b7/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:29dfa0533a5d4c94d4dfa1b694fcb56c9c63aad8330ffdd816fd225d0a7a162f", size = 376055, upload-time = "2026-06-30T07:16:26.111Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/81/fc/a3bcf517084396a6dd258c592567a3c011ba4557f2fde23dceaf26e74f2e/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:af05d726809bff6b141be124d4c7ce998f9c9c7f30edb1f46c07aa103d540b41", size = 494419, upload-time = "2026-06-30T07:16:27.596Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c9/eb/13d529d1788135425c7bf207f8463458ca5d92e43f3f701365b83e9dffc1/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9826217f048f620d9a712672818bf231442c1b35d96b227a07eabd11b4bb6945", size = 384848, upload-time = "2026-06-30T07:16:29.183Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/8e/f4/b7ac49f30013aba8f7b9566b1dd07e81de95e708c1374b7bacc5b9bc5c9c/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:536bceea4fa4acf7e1c61da2b5786304367c816c8895be71b8f537c480b0ea1f", size = 371369, upload-time = "2026-06-30T07:16:30.912Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/31/86/6260bafa622f788b07ddec0e52d810305c8b9b0b8c27f58a2ab04bf62b4f/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:bc0011654b91cc4fb2ae701bec0a0ba1e552c0714247fa7af6c59e0ccfa3a4e1", size = 379673, upload-time = "2026-06-30T07:16:32.486Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/19/c3/03f1ee79a047b48daeca157c89a18509cde22b6b951d642b9b0af1be660a/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:539d75de9e0d536c84ff18dfeb805398e58227001ce09231a26a08b9aed1ee0e", size = 397500, upload-time = "2026-06-30T07:16:34.471Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f0/95/8ed0cd8c377dca12aea498f119fe639fc474d1461545c39d2b5872eb1c0f/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:166cf54d9f44fc6ceb53c7860258dde44a81406646de79f8ed3234fca3b6e538", size = 545978, upload-time = "2026-06-30T07:16:36.45Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d3/f2/0eb57f0eaa83f8fc152a7e03de968ab77e1f00732bebc892b190c6eebde7/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:d34c20167764fbcf927194d532dd7e0c56772f0a5f943fa5ef9e9afbba8fb9db", size = 613350, upload-time = "2026-06-30T07:16:38.213Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/5b/de/e0674bdbc3ef7634989b3f854c3f34bc1f587d36e5bfdc5c378d57034619/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ea7bb13b7c9a29791f87a0387ba7d3ad3a6d783d827e4d3f27b40a0ff44495e2", size = 576486, upload-time = "2026-06-30T07:16:39.797Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f2/f6/21101359743cd136ada781e8210a85769578422ba460672eea0e29739200/rpds_py-2026.6.3-cp314-cp314t-win32.whl", hash = "sha256:6de4744d05bd1aa1be4ed7ea1189e3979196808008113bbbf899a460966b925e", size = 201068, upload-time = "2026-06-30T07:16:41.316Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a6/b2/9574d4d44f7760c2aa32d92a0a4f41698e33f5b204a0bf5c9758f52c79d5/rpds_py-2026.6.3-cp314-cp314t-win_amd64.whl", hash = "sha256:c7b9a2f8f4d8e90af72571d3d495deebdd7e3c75451f5b41719aee166e940fc2", size = 220600, upload-time = "2026-06-30T07:16:43.091Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/08/ae/f23a2697e6ee6340a578b0f136be6483657bef0c6f9497b752bb5c0964bb/rpds_py-2026.6.3-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:e059c5dde6452b44424bd1834557556c226b57781dee1227af23518459722b13", size = 344726, upload-time = "2026-06-30T07:16:44.5Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c3/63/e7b3a1a5358dd32c930a1062d8e15b67fd6e8922e81df9e91706d66ee5c8/rpds_py-2026.6.3-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2f7c26fbc5acd2522b95d4177fe4710ffd8e9b20529e703ffbf8db4d93903f05", size = 339587, upload-time = "2026-06-30T07:16:46.255Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ec/64/10a85681916ca55fffb91b0a211f84e34297c109243484dd6394660a8a7c/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3086b538543802f84c843911242db20447de00d8752dd0efc936dbcf02218ba", size = 369585, upload-time = "2026-06-30T07:16:48.101Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/76/c2/baf95c7c38823e12ba34407c5f5767a89e5cf2233895e56f608167ae9493/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8f2e5c5ee828d42cb11760761c0af6507927bec42d0ad5458f97c9203b054617", size = 375479, upload-time = "2026-06-30T07:16:49.93Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/6a/94/0aad06c72d65101e11d33528d438cda99a39ce0da99466e156158f2541d3/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed0c1e5d10cdc7135537988c74a0188da68e2f3c30813ba3744ab1e42e0480f9", size = 492418, upload-time = "2026-06-30T07:16:51.641Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b5/17/de3f5a479a1f056535d7489819639d8cd591ea6281d700390b43b1abd745/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c2642a7603ec0b16ed77da4555db3b4b472341904873788327c0b0d7b95f1bb", size = 384123, upload-time = "2026-06-30T07:16:53.622Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/46/7d/bf09bd1b145bb2671c03e1e6d1ab8651858d90d8c7dfeadd85a37a934fd8/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e4320744c1ffdd95a603def63344bfab2d33edeab301c5007e7de9f9f5b3885", size = 367351, upload-time = "2026-06-30T07:16:55.241Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a3/ea/1bb734f314b8be319149ddee80b18bd41372bdcfbdf88d28131c0cd37719/rpds_py-2026.6.3-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:a9f4645593036b81bbdb36b9c8e0ea0d1c3fee968c4d59db0344c14087ef143a", size = 378827, upload-time = "2026-06-30T07:16:56.841Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/4b/93/d9611e5b25e26df9a3649813ed66193ace9347a7c7fc4ab7cf70e94851c0/rpds_py-2026.6.3-cp315-cp315-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e55d236be29255554da47abe5c577637db7c24a02b8b46f0ca9524c855801868", size = 395966, upload-time = "2026-06-30T07:16:58.557Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c3/cb/99d77e16e5534ae1d90629bbe419ba6ee170833a6a85e3aa1cc41726fbbc/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:24e9c5386e16669b674a69c156c8eeefcb578f3b3397b713b08e6d60f3c7b187", size = 545680, upload-time = "2026-06-30T07:17:00.164Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/59/15/11a29755f790cef7a2f755e8e14f4f0c33f39489e1893a632a2eee59672b/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:c60924535c75f1566b6eb75b5c31a48a43fef04fa2d0d201acbad8a9969c6107", size = 611853, upload-time = "2026-06-30T07:17:01.962Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/68/86/0c27547e21644da938fb530f7e1a8148dd24d02db07e7a5f2567a17ce710/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:38a2fea2787428f811719ceb9114cb78964a3138838320c29ac39526c79c16ba", size = 573715, upload-time = "2026-06-30T07:17:03.693Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/29/71/4d8fcf700931815594bce892255bbd973b94efaf0fc1932b0590df18d886/rpds_py-2026.6.3-cp315-cp315-win32.whl", hash = "sha256:d483fe17f01ad64b7bf7cc38fcefff1ca9fb83f8c2b2542b68f97ffe0611b369", size = 202864, upload-time = "2026-06-30T07:17:05.746Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/eb/62/b577562de0edbb55b2be85ce5fd09c33e386b9b13eee09833af4240fd5c4/rpds_py-2026.6.3-cp315-cp315-win_amd64.whl", hash = "sha256:67e3a721ffc5d8d2210d3671872298c4a84e4b8035cfe42ffd7cde35d772b146", size = 220430, upload-time = "2026-06-30T07:17:07.471Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c8/95/d6d0b2509825141eef60669a5739eec88dbc6a48053d6c92993a5704defe/rpds_py-2026.6.3-cp315-cp315-win_arm64.whl", hash = "sha256:6e84adbcf4bf841aed8116a8264b9f50b4cb3e7bd89b516122e616ac56ca269e", size = 215877, upload-time = "2026-06-30T07:17:09.008Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b7/bf/f3ea278f0afd615c1d0f19cb69043a41526e2bb600c2b536eb192218eb27/rpds_py-2026.6.3-cp315-cp315t-macosx_10_12_x86_64.whl", hash = "sha256:ae6dd8f10bd17aad820876d24caec9efdafd80a318d16c0a48edb5e136902c6b", size = 346933, upload-time = "2026-06-30T07:17:10.762Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/9d/29/9907bdf1c5346763cf10b7f6852aad86652168c259def904cbe0082c5864/rpds_py-2026.6.3-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:bdbd97738551fca3917c1bd7188bec1920bb520104f28e7e1007f9ceb17b7690", size = 340274, upload-time = "2026-06-30T07:17:12.266Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/6f/2c/8e03767b5778ef25cebf74a7a91a2c3806f8eced4c92cb7406bbe060756d/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b95977e7211527ab0ba576e286d023389fbeeb32a6b7b771665d333c60e5342", size = 370763, upload-time = "2026-06-30T07:17:14.107Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/2e/e1/df2a7e1ba2efd796af26194250b8d42c821b46592311595162af9ef0528d/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d15fde0e6fb0d88a60d221204873743e5d9f0b7d29165e62cd86d0413ad74ba6", size = 376467, upload-time = "2026-06-30T07:17:15.76Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/6b/de/8a0814d1946af29cb068fb259aa8622f856df1d0bab58429448726b537f5/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a136d453475ac0fcbda502ef1e6504bd28d6d904700915d278deeab0d00fe140", size = 496689, upload-time = "2026-06-30T07:17:17.308Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/df/f3/f19e0c852ba13694f5a79f3b719331051573cb5693feacf8a88ffffc3a71/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f826877d462181e5eb1c26a0026b8d0cab05d99844ecb6d8bf3627a2ca0c0442", size = 385340, upload-time = "2026-06-30T07:17:18.928Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e2/ae/7ec3a9d2d4351f99e37bcb06b6b6f954512646bfdbf9742e1de727865daf/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79486287de1730dbaff3dbd124d0ca4d2ef7f9d29bf2544f1f93c09b5bcbbd12", size = 372179, upload-time = "2026-06-30T07:17:20.539Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d3/ac/9cee911dff2aaa9a5a8354f6610bf2e6a616de9197c5fff4f54f82585f1e/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_31_riscv64.whl", hash = "sha256:808345f53cb952433ca2816f1604ff3515608a81784954f38d4452acfe8e61d5", size = 379993, upload-time = "2026-06-30T07:17:22.212Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/83/6b/7c2a07ba88d1e9a936612f7a5d067467ed03d971d5a06f7d309dff044a7e/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1967debc37f64f2c4dc90a7f563aec558b471966e12adcac4e1c4240496b6ebf", size = 398909, upload-time = "2026-06-30T07:17:23.66Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/97/0b/776ffcb66783637b0031f6d58d6fb55913c8b5abf00aeecd46bf933fb477/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:f0840b5b17057f7fd918b76183a4b5a0635f43e14eb2ce60dce1d4ee4707ea00", size = 546584, upload-time = "2026-06-30T07:17:25.264Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/55/33/ba3bc04d7092bd553c9b2b195624992d2cc4f3de1f380b7b93cbee67bd79/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:faa679d19a6696fd54259ad321251ad77a13e70e03dd834daa762a44fb6196ef", size = 614357, upload-time = "2026-06-30T07:17:26.888Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/8b/71/14edf065f04630b1a8472f7653cad03f6c478bcf95ea0e6aed55451e33ea/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:23a439f31ccbeff1574e24889128821d1f7917470e830cf6544dced1c662262a", size = 576533, upload-time = "2026-06-30T07:17:28.546Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ba/76/65002b08596c389105720a8c0d22298b8dc25a4baf89b2ce431343c8b1de/rpds_py-2026.6.3-cp315-cp315t-win32.whl", hash = "sha256:913ca42ccad3f8cc6e292b587ae8ae49c8c823e5dce51a736252fc7c7cdfa577", size = 201204, upload-time = "2026-06-30T07:17:30.193Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/8c/97/d855d6b3c322d1f27e26f5241c42016b56cf01377ea8ed348285f54652f0/rpds_py-2026.6.3-cp315-cp315t-win_amd64.whl", hash = "sha256:ae3d4fe8c0b9213624fdce7279d70e3b148b682ca20719ebd193a23ebfa47324", size = 220719, upload-time = "2026-06-30T07:17:31.788Z" }, + { url = "https://files.pythonhosted.org/packages/5c/be/2e8974163072e7bab7df1a5acd54c4498e75e35d6d18b864d3a9d5dadc92/rpds_py-2026.6.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a0811d33247c3d6128a3001d763f2aa056bb3425204335400ac54f89eec3a0d0", size = 343691, upload-time = "2026-06-30T07:15:14.96Z" }, + { url = "https://files.pythonhosted.org/packages/a4/73/319dfa745dd668efe89309141ded489126461fcecd2b8f3a3cda185129b6/rpds_py-2026.6.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:538949e262e46caa31ac01bdb3c1e8f642622922cacbabbae6a8445d9dc33eaf", size = 338542, upload-time = "2026-06-30T07:15:16.267Z" }, + { url = "https://files.pythonhosted.org/packages/21/63/4239893be1c4d09b709b1a8f6be4188f0870084ff547f46606b8a75f1b03/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55927d532399c2c646100ff7feb48eaa940ad70f42cd68e1328f3ded9f81ca24", size = 368180, upload-time = "2026-06-30T07:15:17.62Z" }, + { url = "https://files.pythonhosted.org/packages/1c/ca/9c5de382225234ceb37b1844ebdb140db12b2a278bb9efe2fcd19f6c82ce/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f56f1695bc5c0871cbc33dc0130fcf503aab0c57dcc5a6700a4f49eba4f2652e", size = 375067, upload-time = "2026-06-30T07:15:18.952Z" }, + { url = "https://files.pythonhosted.org/packages/87/dc/863f69d1bf04ade34b7fe0d59b9fdf6f0135fe2d7cbca74f1d665589559d/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:270b293dae9058fc9fcedab50f13cebf46fb8ed1d1d54e0521a9da5d6b211975", size = 490509, upload-time = "2026-06-30T07:15:20.434Z" }, + { url = "https://files.pythonhosted.org/packages/ce/ef/eac16a12048b45ec7c7fa94f2be3438a5f26bf9cc8580b18a1cfd609b7f6/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:127565fead0a10943b282957bd5447804ff3160ad79f2ad2635e6d249e380680", size = 382754, upload-time = "2026-06-30T07:15:21.831Z" }, + { url = "https://files.pythonhosted.org/packages/04/8f/d2f3f532616be4d06c316ef119683e832bd3d41e112bf3a88f4151c95b17/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ecabd69db66de867690f9797f2f8fa27ba501bbc24540cbdbdc649cd15888ba6", size = 366189, upload-time = "2026-06-30T07:15:23.371Z" }, + { url = "https://files.pythonhosted.org/packages/e3/29/41a7b0e98a4b44cd676ab7598419623373eb43b20be68c084935c1a8cf88/rpds_py-2026.6.3-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:58eadac9cd119677b60e1cf8ac4052f35949d71b8a9e5556efccbe82533cf22a", size = 377750, upload-time = "2026-06-30T07:15:24.659Z" }, + { url = "https://files.pythonhosted.org/packages/2e/05/ecda0bec46f9a1565090bcdc941d023f6a25aff85fda28f89f8d19878152/rpds_py-2026.6.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7491ee23305ac3eb59e492b6945881f5cd77a6f731061a3f25b77fd40f9e99a4", size = 395576, upload-time = "2026-06-30T07:15:25.987Z" }, + { url = "https://files.pythonhosted.org/packages/68/a8/6ed52f03ee6cb854ce78785cc9a9a672eb880e83fd7224d471f667d151f1/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2c99f7e8ccb3dd6e3e4bfeac657a7b208c9bac8075f4b078c02d7404c34107fa", size = 543807, upload-time = "2026-06-30T07:15:27.356Z" }, + { url = "https://files.pythonhosted.org/packages/8f/d6/156c0d3eea27ba09b92562ba2364ba124c0a061b199e17eac637cd25a5e2/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:62698275682bf121181861295c9181e789030a2d516071f5b8f3c23c170cd0fc", size = 611187, upload-time = "2026-06-30T07:15:28.931Z" }, + { url = "https://files.pythonhosted.org/packages/f1/31/774212ed989c62f7f310220089f9b0a3fb8f40f5443d1727abd5d9f52bc9/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a214c993455f99a89aaeadc9b21241900037adc9d97203e374d75513c5911822", size = 573030, upload-time = "2026-06-30T07:15:30.553Z" }, + { url = "https://files.pythonhosted.org/packages/c9/50/22f73127a41f1ce4f87fe39aadfb9a126345801c274aa93ae88456249327/rpds_py-2026.6.3-cp312-cp312-win32.whl", hash = "sha256:501f9f04a588d6a09179368c57071301445191767c64e4b52a6aa9871f1ef5ed", size = 202185, upload-time = "2026-06-30T07:15:32.027Z" }, + { url = "https://files.pythonhosted.org/packages/04/3a/f0ee4d4dde9d3b69dedf1b5f74e7a40017046d55052d173e418c6a94f960/rpds_py-2026.6.3-cp312-cp312-win_amd64.whl", hash = "sha256:2c958bf94822e9290a40aaf2a822d4bc5c88099093e3948ad6c571eca9272e5f", size = 220394, upload-time = "2026-06-30T07:15:33.359Z" }, + { url = "https://files.pythonhosted.org/packages/f3/83/3382fe37f809b59f02aac04dbc4e765b480b46ee0227ed516e3bdc4d3dfc/rpds_py-2026.6.3-cp312-cp312-win_arm64.whl", hash = "sha256:22bffe6042b9bcb0822bcd1955ec00e245daf17b4344e4ed8e9551b976b63e96", size = 215753, upload-time = "2026-06-30T07:15:34.778Z" }, + { url = "https://files.pythonhosted.org/packages/a4/9e/b818ee580026ec578138e961027a68820c40afeb1ec8f6819b54fb99e196/rpds_py-2026.6.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:3cfe765c1da0072636ca06628261e0ea05688e160d5c8a03e0217c3854037223", size = 343012, upload-time = "2026-06-30T07:15:36.005Z" }, + { url = "https://files.pythonhosted.org/packages/f3/6b/686d9dc4359a8f163cfbbf89ee0b4e586431de22fe8248edb63a8cf50d49/rpds_py-2026.6.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f4d78253f6996be4901669ad25319f842f740eccf4d58e3c7f3dd39e6dde1d8f", size = 338203, upload-time = "2026-06-30T07:15:37.462Z" }, + { url = "https://files.pythonhosted.org/packages/9e/9b/069aa329940f8207615e091f5eedbbd40e1e15eac68a0790fd05ccdf796c/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54f45a148e28767bf343d33a684693c70e451c6f4c0e9904709a723fafbdfc1f", size = 367984, upload-time = "2026-06-30T07:15:39.008Z" }, + { url = "https://files.pythonhosted.org/packages/14/db/34c203e4becff3703e4d3bc121842c00b8689197f398161203a880052f4e/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:842e7b070435622248c7a2c44ae53fa1440e073cc3023bc919fed570884097a7", size = 374815, upload-time = "2026-06-30T07:15:40.253Z" }, + { url = "https://files.pythonhosted.org/packages/ee/7d/8071067d2cc453d916ad836e828c943f575e8a44612537759002a1e07381/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8020133a74bd81b4572dd8e4be028a6b1ebcd70e6726edc3918008c08bee6ee6", size = 490545, upload-time = "2026-06-30T07:15:41.729Z" }, + { url = "https://files.pythonhosted.org/packages/a3/42/da06c5aa8f0484ff07f270787434204d9f4535e2f8c3b51ed402267e63c3/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cdc7e35386f3847df728fbcb5e887e2d79c19e2fa1eba9e51b6621d23e3243af", size = 382828, upload-time = "2026-06-30T07:15:43.327Z" }, + { url = "https://files.pythonhosted.org/packages/57/d7/fe978efc2ae50abe48eb7464668ea99f53c010c60aeebb7b35ad27f23661/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acac386b453c2516111b50985d60ce46e7fadb5ea71ae7b25f4c946935bf27cf", size = 365678, upload-time = "2026-06-30T07:15:44.992Z" }, + { url = "https://files.pythonhosted.org/packages/69/9d/1d8922e1990b2a6eb532b6ff53d3e73d2b3bbffc84116c75826bee73dfc6/rpds_py-2026.6.3-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:425560c6fa0415f27261727bb20bd097568485e5eb0c121f1949417d1c516885", size = 377811, upload-time = "2026-06-30T07:15:46.523Z" }, + { url = "https://files.pythonhosted.org/packages/b1/3d/198dceafb4fb034a6a47347e1b0735d34e0bd4a50be4e898d408ee66cb14/rpds_py-2026.6.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a550fb4950a06dde3beb4721f5ad4b25bf4513784665b0a8522c792e2bd822a4", size = 395382, upload-time = "2026-06-30T07:15:47.955Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f1/13968e49655d40b6b19d8b9140296bbc6f1d86b3f0f6c346cf9f1adddf4b/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4f4bca01b63096f606e095734dd56e74e175f94cfbf24ff3d63281cec61f7bb7", size = 543832, upload-time = "2026-06-30T07:15:49.33Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ab/289bcb1b90bd3e40a2900c561fa0e2087345ecbb094f0b870f2345142b7c/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ccffae9a092a00deb7efd545fe5e2c33c33b88e7c054337e9a74c179347d0b7d", size = 611011, upload-time = "2026-06-30T07:15:50.847Z" }, + { url = "https://files.pythonhosted.org/packages/1e/16/5043105e679436ccfbc8e5e0dd2d663ed18a8b8113515fd06a5e5d77c83e/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1cf01971c4f2c5553b772a542e4aaf191789cd331bc2cd4ff0e6e65ba49e1e97", size = 572431, upload-time = "2026-06-30T07:15:52.394Z" }, + { url = "https://files.pythonhosted.org/packages/85/ed/adab103321c0a6565d5ae1c2998349bc3ee175b82ccc5ae8fc04cc413075/rpds_py-2026.6.3-cp313-cp313-win32.whl", hash = "sha256:8c3d1e9c15b9d51ca0391e13da1a25a0a4df3c58a37c9dc368e0736cf7f69df0", size = 201710, upload-time = "2026-06-30T07:15:53.894Z" }, + { url = "https://files.pythonhosted.org/packages/7b/ed/a03b09668e74e5dabbf2e211f6468e1820c0552f7b0500082da31841bf7b/rpds_py-2026.6.3-cp313-cp313-win_amd64.whl", hash = "sha256:9250a9a0a6fd4648b3f868da8d91a4c52b5811a62df58e753d50ae4454a36f80", size = 219454, upload-time = "2026-06-30T07:15:55.25Z" }, + { url = "https://files.pythonhosted.org/packages/27/17/b8642c12930b71bc2b25831f6708ccf0f75abcd11883932ec9ce54ba3a78/rpds_py-2026.6.3-cp313-cp313-win_arm64.whl", hash = "sha256:900a67df3fd1660b035a4761c4ce73c382ea6b35f90f9863c36c6fd8bf8b09bb", size = 215063, upload-time = "2026-06-30T07:15:56.573Z" }, + { url = "https://files.pythonhosted.org/packages/b6/36/7fbe9dcdaf857fb3f63c2a2284b62492d95f5e8334e947e5fb6e7f68c9be/rpds_py-2026.6.3-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:931908d9fc855d8f74783377822be318edb6dcb19e47169dc038f9a1bf60b06e", size = 344510, upload-time = "2026-06-30T07:15:57.921Z" }, + { url = "https://files.pythonhosted.org/packages/ba/54/f785cc3d3f60839ca57a5af4927a9f347b07b2799c373fc20f7949f87c7e/rpds_py-2026.6.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d7469697dce35be237db177d42e2a2ee26e6dcc5fc052078a6fefabd288c6edd", size = 339495, upload-time = "2026-06-30T07:15:59.238Z" }, + { url = "https://files.pythonhosted.org/packages/63/ef/d4cdaf309e6b095b43597103cf8c0b951d6cca2acce68c474f75ec12e0c7/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bcfbcf66006befb9fd2aeaa9e01feaf881b4dc330a02ba07d2322b1c11be7b5d", size = 369454, upload-time = "2026-06-30T07:16:01.021Z" }, + { url = "https://files.pythonhosted.org/packages/96/4a/9559a68b7ee15db09d7981212e8c2e219d2a1d6d4faa0391d813c3496a36/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:847927daf4cffbd4e90e42bc890069897101edd015f956cb8721b3473372edda", size = 374583, upload-time = "2026-06-30T07:16:02.287Z" }, + { url = "https://files.pythonhosted.org/packages/ef/75/8964aa7d2c6e8ac43eba8eb6e6b0fdda1f46d39f2fc3e6aa9f2cb17f485d/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aca6c1ef08a82bfe327cc156da694660f599923e2e6665b6d81c9c2d0ac9ffc8", size = 492919, upload-time = "2026-06-30T07:16:03.723Z" }, + { url = "https://files.pythonhosted.org/packages/8f/97/6908094ac804115e65aedfd90f1b5fee4eebebd3f6c4cfc5419939267565/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ae50181a047c871561212bb97f7932a2d45fb53e947bd9b57ebad85b529cbc53", size = 383725, upload-time = "2026-06-30T07:16:05.305Z" }, + { url = "https://files.pythonhosted.org/packages/d1/9c/0d1fdc2e7aba23e290d603bc494e97bd205bae262ce33c6b32a69768ed5e/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc319e5a1de4b6913aac94bf6a2f9e847371e0a140a43dd4991db1a09bc2d504", size = 367255, upload-time = "2026-06-30T07:16:07.086Z" }, + { url = "https://files.pythonhosted.org/packages/c4/fe/f0209ca4a9ed074bc8acb44dfd0e81c3122e94c9689f5645b7973a866719/rpds_py-2026.6.3-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:e4316bf32babbed84e691e352faf967ce2f0f024174a8643c37c94a1080374fc", size = 379060, upload-time = "2026-06-30T07:16:08.525Z" }, + { url = "https://files.pythonhosted.org/packages/c6/8d/f1cc54c616b9d8897de8738aac148d20afca93f68187475fe194d09a71b9/rpds_py-2026.6.3-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8c6e5a2f750cc71c3e3b11d71661f21d6f9bc6cebc6564b1466417a1ec03ec77", size = 395960, upload-time = "2026-06-30T07:16:09.989Z" }, + { url = "https://files.pythonhosted.org/packages/fb/04/aafff00f73aeca2945f734f1d483c64ab8f472d0864ab02377fd8e89c3b2/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4470ce197d4090875cf6affbf1f853338387428df97c4fb7b7106317b8214698", size = 545356, upload-time = "2026-06-30T07:16:11.816Z" }, + { url = "https://files.pythonhosted.org/packages/fd/cc/e229663b9e4ddac5a4acbe9085dd80a71af2a5d356b8b39d6bff233f24b0/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ea964164cc9afa72d4d9b23cc28dafae93693c0a53e0b42acbff15b22c3f9ddd", size = 612319, upload-time = "2026-06-30T07:16:13.586Z" }, + { url = "https://files.pythonhosted.org/packages/e3/7a/8a0e6d3e6cd066af108b71b43122c3fe158dd9eb86acac626593a2582eb1/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:639c8929aa0afe81be836b04de888460d6bed38b9c54cfc18da8f6bfabf5af5d", size = 573508, upload-time = "2026-06-30T07:16:15.23Z" }, + { url = "https://files.pythonhosted.org/packages/87/03/2a69ab618a789cf6cf85c86bb844c62d090e700ab1a2aa676b3741b6c516/rpds_py-2026.6.3-cp314-cp314-win32.whl", hash = "sha256:882076c00c0a608b131187055ddc5ae29f2e7eaf870d6168980420d58528a5c8", size = 202504, upload-time = "2026-06-30T07:16:16.893Z" }, + { url = "https://files.pythonhosted.org/packages/85/62/a3892ba945f4e24c78f352e5de3c7620d8479f73f211406a97263d13c7d2/rpds_py-2026.6.3-cp314-cp314-win_amd64.whl", hash = "sha256:0be972be84cfcaf46c8c6edf690ca0f154ac17babf1f6a955a51579b34ad2dc5", size = 220380, upload-time = "2026-06-30T07:16:18.108Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e7/c2bd44dc831931815ad11ebb5f430b5a0a4d3caa9de837107876c30c3432/rpds_py-2026.6.3-cp314-cp314-win_arm64.whl", hash = "sha256:2a9c6f195058cb45335e8cc3802745c603d716eb96bc9625950c1aac71c0c703", size = 215976, upload-time = "2026-06-30T07:16:19.654Z" }, + { url = "https://files.pythonhosted.org/packages/79/9c/fff7b74bce9a091ec9a012a03f9ff5f69364eaf9451060dfc4486da2ffdd/rpds_py-2026.6.3-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:f90938e92afda60266da758ee7d363447f7f0138c9559f9e1811629580582d90", size = 346840, upload-time = "2026-06-30T07:16:21.268Z" }, + { url = "https://files.pythonhosted.org/packages/e9/44/77bcb1168b33704908295533d27f10eb811e9e3e193e8993dc99572211d3/rpds_py-2026.6.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ec829541c45bca16e61c7ae50c20501f213605beb75d1aba91a6ee37fbbb56a4", size = 340282, upload-time = "2026-06-30T07:16:22.875Z" }, + { url = "https://files.pythonhosted.org/packages/87/3c/7a9081c7c9e645b39efe19e4ffbeccd80add246327cd9b888aecffd72317/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afd70d95892096cdb26f15a00c45907b17817577aa8d1c76b2dcc2788391f9e9", size = 370403, upload-time = "2026-06-30T07:16:24.415Z" }, + { url = "https://files.pythonhosted.org/packages/f7/69/af47021eb7dad6ff3396cb001c08f0f3c4d06c20253f75be6421a59fe6b7/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:29dfa0533a5d4c94d4dfa1b694fcb56c9c63aad8330ffdd816fd225d0a7a162f", size = 376055, upload-time = "2026-06-30T07:16:26.111Z" }, + { url = "https://files.pythonhosted.org/packages/81/fc/a3bcf517084396a6dd258c592567a3c011ba4557f2fde23dceaf26e74f2e/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:af05d726809bff6b141be124d4c7ce998f9c9c7f30edb1f46c07aa103d540b41", size = 494419, upload-time = "2026-06-30T07:16:27.596Z" }, + { url = "https://files.pythonhosted.org/packages/c9/eb/13d529d1788135425c7bf207f8463458ca5d92e43f3f701365b83e9dffc1/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9826217f048f620d9a712672818bf231442c1b35d96b227a07eabd11b4bb6945", size = 384848, upload-time = "2026-06-30T07:16:29.183Z" }, + { url = "https://files.pythonhosted.org/packages/8e/f4/b7ac49f30013aba8f7b9566b1dd07e81de95e708c1374b7bacc5b9bc5c9c/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:536bceea4fa4acf7e1c61da2b5786304367c816c8895be71b8f537c480b0ea1f", size = 371369, upload-time = "2026-06-30T07:16:30.912Z" }, + { url = "https://files.pythonhosted.org/packages/31/86/6260bafa622f788b07ddec0e52d810305c8b9b0b8c27f58a2ab04bf62b4f/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:bc0011654b91cc4fb2ae701bec0a0ba1e552c0714247fa7af6c59e0ccfa3a4e1", size = 379673, upload-time = "2026-06-30T07:16:32.486Z" }, + { url = "https://files.pythonhosted.org/packages/19/c3/03f1ee79a047b48daeca157c89a18509cde22b6b951d642b9b0af1be660a/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:539d75de9e0d536c84ff18dfeb805398e58227001ce09231a26a08b9aed1ee0e", size = 397500, upload-time = "2026-06-30T07:16:34.471Z" }, + { url = "https://files.pythonhosted.org/packages/f0/95/8ed0cd8c377dca12aea498f119fe639fc474d1461545c39d2b5872eb1c0f/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:166cf54d9f44fc6ceb53c7860258dde44a81406646de79f8ed3234fca3b6e538", size = 545978, upload-time = "2026-06-30T07:16:36.45Z" }, + { url = "https://files.pythonhosted.org/packages/d3/f2/0eb57f0eaa83f8fc152a7e03de968ab77e1f00732bebc892b190c6eebde7/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:d34c20167764fbcf927194d532dd7e0c56772f0a5f943fa5ef9e9afbba8fb9db", size = 613350, upload-time = "2026-06-30T07:16:38.213Z" }, + { url = "https://files.pythonhosted.org/packages/5b/de/e0674bdbc3ef7634989b3f854c3f34bc1f587d36e5bfdc5c378d57034619/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ea7bb13b7c9a29791f87a0387ba7d3ad3a6d783d827e4d3f27b40a0ff44495e2", size = 576486, upload-time = "2026-06-30T07:16:39.797Z" }, + { url = "https://files.pythonhosted.org/packages/f2/f6/21101359743cd136ada781e8210a85769578422ba460672eea0e29739200/rpds_py-2026.6.3-cp314-cp314t-win32.whl", hash = "sha256:6de4744d05bd1aa1be4ed7ea1189e3979196808008113bbbf899a460966b925e", size = 201068, upload-time = "2026-06-30T07:16:41.316Z" }, + { url = "https://files.pythonhosted.org/packages/a6/b2/9574d4d44f7760c2aa32d92a0a4f41698e33f5b204a0bf5c9758f52c79d5/rpds_py-2026.6.3-cp314-cp314t-win_amd64.whl", hash = "sha256:c7b9a2f8f4d8e90af72571d3d495deebdd7e3c75451f5b41719aee166e940fc2", size = 220600, upload-time = "2026-06-30T07:16:43.091Z" }, + { url = "https://files.pythonhosted.org/packages/08/ae/f23a2697e6ee6340a578b0f136be6483657bef0c6f9497b752bb5c0964bb/rpds_py-2026.6.3-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:e059c5dde6452b44424bd1834557556c226b57781dee1227af23518459722b13", size = 344726, upload-time = "2026-06-30T07:16:44.5Z" }, + { url = "https://files.pythonhosted.org/packages/c3/63/e7b3a1a5358dd32c930a1062d8e15b67fd6e8922e81df9e91706d66ee5c8/rpds_py-2026.6.3-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2f7c26fbc5acd2522b95d4177fe4710ffd8e9b20529e703ffbf8db4d93903f05", size = 339587, upload-time = "2026-06-30T07:16:46.255Z" }, + { url = "https://files.pythonhosted.org/packages/ec/64/10a85681916ca55fffb91b0a211f84e34297c109243484dd6394660a8a7c/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3086b538543802f84c843911242db20447de00d8752dd0efc936dbcf02218ba", size = 369585, upload-time = "2026-06-30T07:16:48.101Z" }, + { url = "https://files.pythonhosted.org/packages/76/c2/baf95c7c38823e12ba34407c5f5767a89e5cf2233895e56f608167ae9493/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8f2e5c5ee828d42cb11760761c0af6507927bec42d0ad5458f97c9203b054617", size = 375479, upload-time = "2026-06-30T07:16:49.93Z" }, + { url = "https://files.pythonhosted.org/packages/6a/94/0aad06c72d65101e11d33528d438cda99a39ce0da99466e156158f2541d3/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed0c1e5d10cdc7135537988c74a0188da68e2f3c30813ba3744ab1e42e0480f9", size = 492418, upload-time = "2026-06-30T07:16:51.641Z" }, + { url = "https://files.pythonhosted.org/packages/b5/17/de3f5a479a1f056535d7489819639d8cd591ea6281d700390b43b1abd745/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c2642a7603ec0b16ed77da4555db3b4b472341904873788327c0b0d7b95f1bb", size = 384123, upload-time = "2026-06-30T07:16:53.622Z" }, + { url = "https://files.pythonhosted.org/packages/46/7d/bf09bd1b145bb2671c03e1e6d1ab8651858d90d8c7dfeadd85a37a934fd8/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e4320744c1ffdd95a603def63344bfab2d33edeab301c5007e7de9f9f5b3885", size = 367351, upload-time = "2026-06-30T07:16:55.241Z" }, + { url = "https://files.pythonhosted.org/packages/a3/ea/1bb734f314b8be319149ddee80b18bd41372bdcfbdf88d28131c0cd37719/rpds_py-2026.6.3-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:a9f4645593036b81bbdb36b9c8e0ea0d1c3fee968c4d59db0344c14087ef143a", size = 378827, upload-time = "2026-06-30T07:16:56.841Z" }, + { url = "https://files.pythonhosted.org/packages/4b/93/d9611e5b25e26df9a3649813ed66193ace9347a7c7fc4ab7cf70e94851c0/rpds_py-2026.6.3-cp315-cp315-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e55d236be29255554da47abe5c577637db7c24a02b8b46f0ca9524c855801868", size = 395966, upload-time = "2026-06-30T07:16:58.557Z" }, + { url = "https://files.pythonhosted.org/packages/c3/cb/99d77e16e5534ae1d90629bbe419ba6ee170833a6a85e3aa1cc41726fbbc/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:24e9c5386e16669b674a69c156c8eeefcb578f3b3397b713b08e6d60f3c7b187", size = 545680, upload-time = "2026-06-30T07:17:00.164Z" }, + { url = "https://files.pythonhosted.org/packages/59/15/11a29755f790cef7a2f755e8e14f4f0c33f39489e1893a632a2eee59672b/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:c60924535c75f1566b6eb75b5c31a48a43fef04fa2d0d201acbad8a9969c6107", size = 611853, upload-time = "2026-06-30T07:17:01.962Z" }, + { url = "https://files.pythonhosted.org/packages/68/86/0c27547e21644da938fb530f7e1a8148dd24d02db07e7a5f2567a17ce710/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:38a2fea2787428f811719ceb9114cb78964a3138838320c29ac39526c79c16ba", size = 573715, upload-time = "2026-06-30T07:17:03.693Z" }, + { url = "https://files.pythonhosted.org/packages/29/71/4d8fcf700931815594bce892255bbd973b94efaf0fc1932b0590df18d886/rpds_py-2026.6.3-cp315-cp315-win32.whl", hash = "sha256:d483fe17f01ad64b7bf7cc38fcefff1ca9fb83f8c2b2542b68f97ffe0611b369", size = 202864, upload-time = "2026-06-30T07:17:05.746Z" }, + { url = "https://files.pythonhosted.org/packages/eb/62/b577562de0edbb55b2be85ce5fd09c33e386b9b13eee09833af4240fd5c4/rpds_py-2026.6.3-cp315-cp315-win_amd64.whl", hash = "sha256:67e3a721ffc5d8d2210d3671872298c4a84e4b8035cfe42ffd7cde35d772b146", size = 220430, upload-time = "2026-06-30T07:17:07.471Z" }, + { url = "https://files.pythonhosted.org/packages/c8/95/d6d0b2509825141eef60669a5739eec88dbc6a48053d6c92993a5704defe/rpds_py-2026.6.3-cp315-cp315-win_arm64.whl", hash = "sha256:6e84adbcf4bf841aed8116a8264b9f50b4cb3e7bd89b516122e616ac56ca269e", size = 215877, upload-time = "2026-06-30T07:17:09.008Z" }, + { url = "https://files.pythonhosted.org/packages/b7/bf/f3ea278f0afd615c1d0f19cb69043a41526e2bb600c2b536eb192218eb27/rpds_py-2026.6.3-cp315-cp315t-macosx_10_12_x86_64.whl", hash = "sha256:ae6dd8f10bd17aad820876d24caec9efdafd80a318d16c0a48edb5e136902c6b", size = 346933, upload-time = "2026-06-30T07:17:10.762Z" }, + { url = "https://files.pythonhosted.org/packages/9d/29/9907bdf1c5346763cf10b7f6852aad86652168c259def904cbe0082c5864/rpds_py-2026.6.3-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:bdbd97738551fca3917c1bd7188bec1920bb520104f28e7e1007f9ceb17b7690", size = 340274, upload-time = "2026-06-30T07:17:12.266Z" }, + { url = "https://files.pythonhosted.org/packages/6f/2c/8e03767b5778ef25cebf74a7a91a2c3806f8eced4c92cb7406bbe060756d/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b95977e7211527ab0ba576e286d023389fbeeb32a6b7b771665d333c60e5342", size = 370763, upload-time = "2026-06-30T07:17:14.107Z" }, + { url = "https://files.pythonhosted.org/packages/2e/e1/df2a7e1ba2efd796af26194250b8d42c821b46592311595162af9ef0528d/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d15fde0e6fb0d88a60d221204873743e5d9f0b7d29165e62cd86d0413ad74ba6", size = 376467, upload-time = "2026-06-30T07:17:15.76Z" }, + { url = "https://files.pythonhosted.org/packages/6b/de/8a0814d1946af29cb068fb259aa8622f856df1d0bab58429448726b537f5/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a136d453475ac0fcbda502ef1e6504bd28d6d904700915d278deeab0d00fe140", size = 496689, upload-time = "2026-06-30T07:17:17.308Z" }, + { url = "https://files.pythonhosted.org/packages/df/f3/f19e0c852ba13694f5a79f3b719331051573cb5693feacf8a88ffffc3a71/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f826877d462181e5eb1c26a0026b8d0cab05d99844ecb6d8bf3627a2ca0c0442", size = 385340, upload-time = "2026-06-30T07:17:18.928Z" }, + { url = "https://files.pythonhosted.org/packages/e2/ae/7ec3a9d2d4351f99e37bcb06b6b6f954512646bfdbf9742e1de727865daf/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79486287de1730dbaff3dbd124d0ca4d2ef7f9d29bf2544f1f93c09b5bcbbd12", size = 372179, upload-time = "2026-06-30T07:17:20.539Z" }, + { url = "https://files.pythonhosted.org/packages/d3/ac/9cee911dff2aaa9a5a8354f6610bf2e6a616de9197c5fff4f54f82585f1e/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_31_riscv64.whl", hash = "sha256:808345f53cb952433ca2816f1604ff3515608a81784954f38d4452acfe8e61d5", size = 379993, upload-time = "2026-06-30T07:17:22.212Z" }, + { url = "https://files.pythonhosted.org/packages/83/6b/7c2a07ba88d1e9a936612f7a5d067467ed03d971d5a06f7d309dff044a7e/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1967debc37f64f2c4dc90a7f563aec558b471966e12adcac4e1c4240496b6ebf", size = 398909, upload-time = "2026-06-30T07:17:23.66Z" }, + { url = "https://files.pythonhosted.org/packages/97/0b/776ffcb66783637b0031f6d58d6fb55913c8b5abf00aeecd46bf933fb477/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:f0840b5b17057f7fd918b76183a4b5a0635f43e14eb2ce60dce1d4ee4707ea00", size = 546584, upload-time = "2026-06-30T07:17:25.264Z" }, + { url = "https://files.pythonhosted.org/packages/55/33/ba3bc04d7092bd553c9b2b195624992d2cc4f3de1f380b7b93cbee67bd79/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:faa679d19a6696fd54259ad321251ad77a13e70e03dd834daa762a44fb6196ef", size = 614357, upload-time = "2026-06-30T07:17:26.888Z" }, + { url = "https://files.pythonhosted.org/packages/8b/71/14edf065f04630b1a8472f7653cad03f6c478bcf95ea0e6aed55451e33ea/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:23a439f31ccbeff1574e24889128821d1f7917470e830cf6544dced1c662262a", size = 576533, upload-time = "2026-06-30T07:17:28.546Z" }, + { url = "https://files.pythonhosted.org/packages/ba/76/65002b08596c389105720a8c0d22298b8dc25a4baf89b2ce431343c8b1de/rpds_py-2026.6.3-cp315-cp315t-win32.whl", hash = "sha256:913ca42ccad3f8cc6e292b587ae8ae49c8c823e5dce51a736252fc7c7cdfa577", size = 201204, upload-time = "2026-06-30T07:17:30.193Z" }, + { url = "https://files.pythonhosted.org/packages/8c/97/d855d6b3c322d1f27e26f5241c42016b56cf01377ea8ed348285f54652f0/rpds_py-2026.6.3-cp315-cp315t-win_amd64.whl", hash = "sha256:ae3d4fe8c0b9213624fdce7279d70e3b148b682ca20719ebd193a23ebfa47324", size = 220719, upload-time = "2026-06-30T07:17:31.788Z" }, ] [[package]] name = "runtime" -version = "0.1.0" +version = "0.2.0" source = { editable = "runtime" } dependencies = [ + { name = "common" }, { name = "fastapi" }, - { name = "loguru" }, - { name = "notebook" }, - { name = "pydantic" }, - { name = "uvicorn" }, + { name = "httpx" }, + { name = "redis" }, + { name = "uvicorn", extra = ["standard"] }, ] [package.metadata] requires-dist = [ - { name = "fastapi", specifier = ">=0.140.0" }, - { name = "loguru", specifier = ">=0.7.3" }, - { name = "notebook", specifier = ">=7.6.1" }, - { name = "pydantic", specifier = ">=2.13.4" }, - { name = "uvicorn", specifier = ">=0.51.0" }, + { name = "common", editable = "common" }, + { name = "fastapi", specifier = "==0.116.1" }, + { name = "httpx", specifier = "==0.28.1" }, + { name = "redis", specifier = "==5.2.1" }, + { name = "uvicorn", extras = ["standard"], specifier = "==0.35.0" }, ] [[package]] name = "s3transfer" version = "0.19.2" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" } +source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "botocore" }, ] -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/76/43/35e4d8aa320bffe8287fe8f65f578fa2d2db0a64212f0e710dce58267854/s3transfer-0.19.2.tar.gz", hash = "sha256:ba0309fd86be3c27dbf78cdd813c13c5e1df16e5874b99d2535ebbdfb9892993", size = 165592, upload-time = "2026-07-22T19:30:44.432Z" } +sdist = { url = "https://files.pythonhosted.org/packages/76/43/35e4d8aa320bffe8287fe8f65f578fa2d2db0a64212f0e710dce58267854/s3transfer-0.19.2.tar.gz", hash = "sha256:ba0309fd86be3c27dbf78cdd813c13c5e1df16e5874b99d2535ebbdfb9892993", size = 165592, upload-time = "2026-07-22T19:30:44.432Z" } wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/bc/e7/5c595c75e9f41a44f30e526eda465ea0b4eec93470e074e4a111b253f13a/s3transfer-0.19.2-py3-none-any.whl", hash = "sha256:d8168eccca828cbb2cd573675333f3bddd254313a9c42494b84c76b539e8ba25", size = 90216, upload-time = "2026-07-22T19:30:43.251Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e7/5c595c75e9f41a44f30e526eda465ea0b4eec93470e074e4a111b253f13a/s3transfer-0.19.2-py3-none-any.whl", hash = "sha256:d8168eccca828cbb2cd573675333f3bddd254313a9c42494b84c76b539e8ba25", size = 90216, upload-time = "2026-07-22T19:30:43.251Z" }, ] [[package]] name = "schedule" -version = "0.1.0" +version = "0.2.0" source = { editable = "schedule" } +dependencies = [ + { name = "common" }, + { name = "fastapi" }, + { name = "httpx" }, + { name = "ipykernel" }, + { name = "nbclient" }, + { name = "nbformat" }, + { name = "redis" }, + { name = "uvicorn", extra = ["standard"] }, +] -[[package]] -name = "send2trash" -version = "2.1.0" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" } -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c5/f0/184b4b5f8d00f2a92cf96eec8967a3d550b52cf94362dad1100df9e48d57/send2trash-2.1.0.tar.gz", hash = "sha256:1c72b39f09457db3c05ce1d19158c2cbef4c32b8bedd02c155e49282b7ea7459", size = 17255, upload-time = "2026-01-14T06:27:36.056Z" } -wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/1c/78/504fdd027da3b84ff1aecd9f6957e65f35134534ccc6da8628eb71e76d3f/send2trash-2.1.0-py3-none-any.whl", hash = "sha256:0da2f112e6d6bb22de6aa6daa7e144831a4febf2a87261451c4ad849fe9a873c", size = 17610, upload-time = "2026-01-14T06:27:35.218Z" }, +[package.metadata] +requires-dist = [ + { name = "common", editable = "common" }, + { name = "fastapi", specifier = "==0.116.1" }, + { name = "httpx", specifier = "==0.28.1" }, + { name = "ipykernel", specifier = "==6.29.5" }, + { name = "nbclient", specifier = "==0.10.2" }, + { name = "nbformat", specifier = "==5.10.4" }, + { name = "redis", specifier = "==5.2.1" }, + { name = "uvicorn", extras = ["standard"], specifier = "==0.35.0" }, ] [[package]] name = "six" version = "1.17.0" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" } -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, -] - -[[package]] -name = "soupsieve" -version = "2.9.1" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" } -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d9/38/e12680bbe6b4f8f3d17adcaf38d26850aa756c85cf4a80e79fc12a018fe8/soupsieve-2.9.1.tar.gz", hash = "sha256:c33e6605bbc71dd628b00c632d58ae607c22bade247e52553928f83bbb75b4ba", size = 122261, upload-time = "2026-07-21T16:57:17.452Z" } -wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/0f/2c/437fe806897c2d6cfdc3ee43a18da8bf8e568530a4ae9bac781541ca9896/soupsieve-2.9.1-py3-none-any.whl", hash = "sha256:4f4477399246b7a0c720a88ca2454b11cd6bb9ae4c9d170140786e916776c14c", size = 37404, upload-time = "2026-07-21T16:57:16.421Z" }, + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, ] [[package]] name = "sqlalchemy" version = "2.0.51" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" } +source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" }, { name = "typing-extensions" }, ] -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/02/f1/a7a892f18d4d224e6b26f706531eafccc41e37594d37d304786969ee13cb/sqlalchemy-2.0.51.tar.gz", hash = "sha256:804dccd8a4a6242c4e30ad961e540e18a588f6527202f2d6791b01845d59fdc9", size = 9912201, upload-time = "2026-06-15T15:41:20.012Z" } +sdist = { url = "https://files.pythonhosted.org/packages/02/f1/a7a892f18d4d224e6b26f706531eafccc41e37594d37d304786969ee13cb/sqlalchemy-2.0.51.tar.gz", hash = "sha256:804dccd8a4a6242c4e30ad961e540e18a588f6527202f2d6791b01845d59fdc9", size = 9912201, upload-time = "2026-06-15T15:41:20.012Z" } wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d5/70/e868bc5412acd101a8280f25c95f10eeae0771c4eb806b02491142810ee8/sqlalchemy-2.0.51-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d78702b26ba1c18b2d0fb2ea940ba7f17a9581b42e8361ff93920ebbee1235a", size = 2160291, upload-time = "2026-06-15T16:08:48.918Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e5/1c/71ee0f8a6b9d7316a1ccd30430b4c62b6c2e36adc96017a4e3a72dce49d6/sqlalchemy-2.0.51-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581921d849d6e6f994d560389192955e80e2950e18fcdfe2ccea863e01158e6e", size = 3343835, upload-time = "2026-06-15T16:19:42.613Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/2b/7c/7ab9f9aadc5944fdd06612484ed7918fe376ad871a5f50404dc1536e0194/sqlalchemy-2.0.51-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1d21ce524ab86c23046e992a5b81cb54c21079c6df6e78b8fc77d77cac70a6b9", size = 3358470, upload-time = "2026-06-15T16:26:38.011Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d0/7d/ff77169fee6186de145a7f2b87006c39638391130abbab2b1f63ac6ea583/sqlalchemy-2.0.51-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c5d98a2709840027f5a347c3af0a7c3d5f6c1ff93af2ca1c54494e23cba8f389", size = 3289874, upload-time = "2026-06-15T16:19:45.212Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/6f/3b/6c505903710d781b55bc3141ee34a062bf9745a6b5bc7333305b9ed63b33/sqlalchemy-2.0.51-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1181256e0f16479691b5616d36375dc2620ad8332b25978763c3d206ad3f3f1d", size = 3321692, upload-time = "2026-06-15T16:26:39.747Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/3c/b7/c5ffe50aa2f4d947c9250e1519d939260329a07fe6272edfccd784b3d007/sqlalchemy-2.0.51-cp312-cp312-win32.whl", hash = "sha256:9f380393be5abeb6815f68fd39271b95127173511b6706b0a630a9995d53f8f5", size = 2119674, upload-time = "2026-06-15T16:23:09.543Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/25/dc/46a65916af68a06ef6b972c6050ba4c8f97070fe3fb33097d34229d9bef6/sqlalchemy-2.0.51-cp312-cp312-win_amd64.whl", hash = "sha256:2cf39aabdf48e87c1c2c2ed6d20d33ffa0733b3071ce9c5f66357947dd009080", size = 2146670, upload-time = "2026-06-15T16:23:11.048Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/54/fe/a210d52fd1a90ecfae8a78e9d8b27e18d733d60818a8bf250ff690b75120/sqlalchemy-2.0.51-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7c2056838b6685b72fdb36c99996cf862753461a62f2e84f4196371d3b2d6a07", size = 2157184, upload-time = "2026-06-15T16:08:50.374Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/17/6b/2dce8369b199cb855110e056032f94a9f66dacc2237d3d39c115a86eac56/sqlalchemy-2.0.51-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:483b11bd46bf35fc14c52faf338b04300c9e6ce554bce9b11be85bfec3bc3195", size = 3284735, upload-time = "2026-06-15T16:19:46.934Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/53/ff/dbc495b8a14da840faffb353857a72d4190113cac33727906fb997047f0f/sqlalchemy-2.0.51-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1bed1ee8b01da6088210aa9412023326fb98a599ba502e6118308601dcbef77f", size = 3302756, upload-time = "2026-06-15T16:26:41.336Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/cf/d5/fde8f4dddcf518ee15ab35a7c6a28acc32c8ba548d1d2aa451f96e6dbb0b/sqlalchemy-2.0.51-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:72ca54c952107ba5cd58854b67a5a6268631289d21651a1235396f3b98b47400", size = 3232055, upload-time = "2026-06-15T16:19:49.286Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/67/d1/43d3a0ac955a58601c24fa23038b1c55ee3a1ec02c0f96ebb1eae2bcf614/sqlalchemy-2.0.51-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b3e693d15533a45cd5906f0589f9c35090bef6ef45bf1e8195c424aa0ae06a8d", size = 3269850, upload-time = "2026-06-15T16:26:43.017Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/94/df/de669c7054cd47c4439ac34b1b2ee8b804a794791fbb10720e997a2c87c7/sqlalchemy-2.0.51-cp313-cp313-win32.whl", hash = "sha256:b93ab07b5292dbe7e6b8da89475275e7042744283921344b56105f3eeb0f828b", size = 2117721, upload-time = "2026-06-15T16:23:12.36Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d0/8a/403c51d064196bae20a0bc2476577f83a3f8dd299719a97417086b7f2ec5/sqlalchemy-2.0.51-cp313-cp313-win_amd64.whl", hash = "sha256:0f053118c30e53161857a953e4de667d90e274980dccbe5dd3829bbbeece72a5", size = 2143615, upload-time = "2026-06-15T16:23:13.906Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b1/49/a739be2e1d02a96a658eb71ab45d921c874249252358ad24a5bffdd02525/sqlalchemy-2.0.51-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6ea306caaae6bd5afd0a46050003c88f6bf33227377a49298c498c3cb88ff491", size = 2158999, upload-time = "2026-06-15T16:08:51.759Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/23/6b/2e0e38cf75c8780eca78d9b2e78164f8bcfd70125e5caa588ff5cbb9c9f4/sqlalchemy-2.0.51-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c45a496d6bc05dec41dcd4c3a2b183723f47473255c159cd80b503c8f246424d", size = 3282539, upload-time = "2026-06-15T16:19:51.065Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/dd/a1/e77854cb5336fd37dc3c6ae3b71de242c98caac5725120be0b526b31cbd0/sqlalchemy-2.0.51-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4004ada0aafe8ae1991b2cd1d99c6d9146126e123bd6f883c260d974aa012e54", size = 3287545, upload-time = "2026-06-15T16:26:44.735Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f6/ab/9e17272fd4dac8df3b83c4fbe52b998a1c9d89a843c8c35ff29b74ff7364/sqlalchemy-2.0.51-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0f6bcad487aee1c638d707235682fc96f741de00663619881ab235400d03289e", size = 3230929, upload-time = "2026-06-15T16:19:52.625Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/02/3c/52f408ea701781caee975606beccc48845f2aee8711ac29843d612c0306c/sqlalchemy-2.0.51-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:39a76529db6305693d8d4affa58ad5b5e2e18edd62daea628b29b97930b3513d", size = 3252888, upload-time = "2026-06-15T16:26:46.454Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/24/16/3efd2ee6bc4ca4693a30a1dd17a91b606cae15d517d2a4746611d9b73ce8/sqlalchemy-2.0.51-cp314-cp314-win32.whl", hash = "sha256:08a204d8b5638717c26a24df18fcf40af45a6b22e35b70b1d62f0113c2e278e8", size = 2120551, upload-time = "2026-06-15T16:23:15.629Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/7b/78/55b12e70f45bccc40d9e483925c065027b3b98ea4cbbdf6f8c2546feaf6c/sqlalchemy-2.0.51-cp314-cp314-win_amd64.whl", hash = "sha256:96747bfbadb055466e5b46d572618170046b45ce5a4879167f50d70a5319a499", size = 2146318, upload-time = "2026-06-15T16:23:17.108Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/21/db/a9574ed40fed418924b1b1a3e54f47ee3963053b3d3d325a0d36b41f2c08/sqlalchemy-2.0.51-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e5ea1a213be1fcd5e49d9904c3b9939211ded90bc2a64e93f4c01963474285de", size = 2178920, upload-time = "2026-06-15T15:59:56.285Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/bf/90/a1bb5c7cbba76b7bc1fbd586d0a5479a7bc9c27b4a8298f22ec9423b2bb3/sqlalchemy-2.0.51-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7c6b36ed71f41942bdcd2ad2522be46bfce09d5705be5640ecf19bbc7660e4b7", size = 3566534, upload-time = "2026-06-15T15:58:35.024Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/15/4b/481f1fed30e0e9e8dd24aecbb49f29eb57fe7657ece5cf06ee9b84bb97d8/sqlalchemy-2.0.51-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0c2c62877097e1a0db401fba5cb4debee33265e5b2a55c4ccb489c02c53b4f72", size = 3535844, upload-time = "2026-06-15T16:02:43.973Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/02/71/0aa64aeda645510af0a43f7d9ee70932f0d1dc4263aed34c50ee891d9df3/sqlalchemy-2.0.51-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0378d055e9e8cd6ce4d8dff683bdd3d7d413533c4ee51d67a2b1e0f9eacc0f23", size = 3475355, upload-time = "2026-06-15T15:58:36.592Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/05/db/6061db32316446135a3abae5f308d144ab988a34234726042da3e58b1c63/sqlalchemy-2.0.51-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6e46fc36029eff666391e0531e5387b62ce6c4f1d8e50b3fb3099eaca1b42522", size = 3486591, upload-time = "2026-06-15T16:02:45.346Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/0d/c9/f14fdf71bb8957e0c7e39db69bbdf12b5c80f4ef775fdfa127bf4e0d6760/sqlalchemy-2.0.51-cp314-cp314t-win32.whl", hash = "sha256:9161cfc9efce70d1715f47d6ff40f79c6778c00d53be4fbc09d70301e4b83ba7", size = 2151313, upload-time = "2026-06-15T16:03:39.127Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/6a/c6/673e618e6f4f297e126d9b56ea2f6478708f6c1af4e3223835c22e2c3697/sqlalchemy-2.0.51-cp314-cp314t-win_amd64.whl", hash = "sha256:159bb6ba32059f57ad7375a8f50d844dd2f19d14954ecf820cd33e20debd46b2", size = 2186280, upload-time = "2026-06-15T16:03:40.569Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e2/22/dbf013a12ec759e54a34a119e9e217435b3f71b2dd5c61a7ade0a25dae87/sqlalchemy-2.0.51-py3-none-any.whl", hash = "sha256:bb024d8b621d0be75f4f44ecc7c950450026e76d66dc8f791bb5331d7fed59d5", size = 1944334, upload-time = "2026-06-15T16:09:22.418Z" }, + { url = "https://files.pythonhosted.org/packages/d5/70/e868bc5412acd101a8280f25c95f10eeae0771c4eb806b02491142810ee8/sqlalchemy-2.0.51-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d78702b26ba1c18b2d0fb2ea940ba7f17a9581b42e8361ff93920ebbee1235a", size = 2160291, upload-time = "2026-06-15T16:08:48.918Z" }, + { url = "https://files.pythonhosted.org/packages/e5/1c/71ee0f8a6b9d7316a1ccd30430b4c62b6c2e36adc96017a4e3a72dce49d6/sqlalchemy-2.0.51-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581921d849d6e6f994d560389192955e80e2950e18fcdfe2ccea863e01158e6e", size = 3343835, upload-time = "2026-06-15T16:19:42.613Z" }, + { url = "https://files.pythonhosted.org/packages/2b/7c/7ab9f9aadc5944fdd06612484ed7918fe376ad871a5f50404dc1536e0194/sqlalchemy-2.0.51-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1d21ce524ab86c23046e992a5b81cb54c21079c6df6e78b8fc77d77cac70a6b9", size = 3358470, upload-time = "2026-06-15T16:26:38.011Z" }, + { url = "https://files.pythonhosted.org/packages/d0/7d/ff77169fee6186de145a7f2b87006c39638391130abbab2b1f63ac6ea583/sqlalchemy-2.0.51-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c5d98a2709840027f5a347c3af0a7c3d5f6c1ff93af2ca1c54494e23cba8f389", size = 3289874, upload-time = "2026-06-15T16:19:45.212Z" }, + { url = "https://files.pythonhosted.org/packages/6f/3b/6c505903710d781b55bc3141ee34a062bf9745a6b5bc7333305b9ed63b33/sqlalchemy-2.0.51-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1181256e0f16479691b5616d36375dc2620ad8332b25978763c3d206ad3f3f1d", size = 3321692, upload-time = "2026-06-15T16:26:39.747Z" }, + { url = "https://files.pythonhosted.org/packages/3c/b7/c5ffe50aa2f4d947c9250e1519d939260329a07fe6272edfccd784b3d007/sqlalchemy-2.0.51-cp312-cp312-win32.whl", hash = "sha256:9f380393be5abeb6815f68fd39271b95127173511b6706b0a630a9995d53f8f5", size = 2119674, upload-time = "2026-06-15T16:23:09.543Z" }, + { url = "https://files.pythonhosted.org/packages/25/dc/46a65916af68a06ef6b972c6050ba4c8f97070fe3fb33097d34229d9bef6/sqlalchemy-2.0.51-cp312-cp312-win_amd64.whl", hash = "sha256:2cf39aabdf48e87c1c2c2ed6d20d33ffa0733b3071ce9c5f66357947dd009080", size = 2146670, upload-time = "2026-06-15T16:23:11.048Z" }, + { url = "https://files.pythonhosted.org/packages/54/fe/a210d52fd1a90ecfae8a78e9d8b27e18d733d60818a8bf250ff690b75120/sqlalchemy-2.0.51-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7c2056838b6685b72fdb36c99996cf862753461a62f2e84f4196371d3b2d6a07", size = 2157184, upload-time = "2026-06-15T16:08:50.374Z" }, + { url = "https://files.pythonhosted.org/packages/17/6b/2dce8369b199cb855110e056032f94a9f66dacc2237d3d39c115a86eac56/sqlalchemy-2.0.51-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:483b11bd46bf35fc14c52faf338b04300c9e6ce554bce9b11be85bfec3bc3195", size = 3284735, upload-time = "2026-06-15T16:19:46.934Z" }, + { url = "https://files.pythonhosted.org/packages/53/ff/dbc495b8a14da840faffb353857a72d4190113cac33727906fb997047f0f/sqlalchemy-2.0.51-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1bed1ee8b01da6088210aa9412023326fb98a599ba502e6118308601dcbef77f", size = 3302756, upload-time = "2026-06-15T16:26:41.336Z" }, + { url = "https://files.pythonhosted.org/packages/cf/d5/fde8f4dddcf518ee15ab35a7c6a28acc32c8ba548d1d2aa451f96e6dbb0b/sqlalchemy-2.0.51-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:72ca54c952107ba5cd58854b67a5a6268631289d21651a1235396f3b98b47400", size = 3232055, upload-time = "2026-06-15T16:19:49.286Z" }, + { url = "https://files.pythonhosted.org/packages/67/d1/43d3a0ac955a58601c24fa23038b1c55ee3a1ec02c0f96ebb1eae2bcf614/sqlalchemy-2.0.51-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b3e693d15533a45cd5906f0589f9c35090bef6ef45bf1e8195c424aa0ae06a8d", size = 3269850, upload-time = "2026-06-15T16:26:43.017Z" }, + { url = "https://files.pythonhosted.org/packages/94/df/de669c7054cd47c4439ac34b1b2ee8b804a794791fbb10720e997a2c87c7/sqlalchemy-2.0.51-cp313-cp313-win32.whl", hash = "sha256:b93ab07b5292dbe7e6b8da89475275e7042744283921344b56105f3eeb0f828b", size = 2117721, upload-time = "2026-06-15T16:23:12.36Z" }, + { url = "https://files.pythonhosted.org/packages/d0/8a/403c51d064196bae20a0bc2476577f83a3f8dd299719a97417086b7f2ec5/sqlalchemy-2.0.51-cp313-cp313-win_amd64.whl", hash = "sha256:0f053118c30e53161857a953e4de667d90e274980dccbe5dd3829bbbeece72a5", size = 2143615, upload-time = "2026-06-15T16:23:13.906Z" }, + { url = "https://files.pythonhosted.org/packages/b1/49/a739be2e1d02a96a658eb71ab45d921c874249252358ad24a5bffdd02525/sqlalchemy-2.0.51-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6ea306caaae6bd5afd0a46050003c88f6bf33227377a49298c498c3cb88ff491", size = 2158999, upload-time = "2026-06-15T16:08:51.759Z" }, + { url = "https://files.pythonhosted.org/packages/23/6b/2e0e38cf75c8780eca78d9b2e78164f8bcfd70125e5caa588ff5cbb9c9f4/sqlalchemy-2.0.51-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c45a496d6bc05dec41dcd4c3a2b183723f47473255c159cd80b503c8f246424d", size = 3282539, upload-time = "2026-06-15T16:19:51.065Z" }, + { url = "https://files.pythonhosted.org/packages/dd/a1/e77854cb5336fd37dc3c6ae3b71de242c98caac5725120be0b526b31cbd0/sqlalchemy-2.0.51-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4004ada0aafe8ae1991b2cd1d99c6d9146126e123bd6f883c260d974aa012e54", size = 3287545, upload-time = "2026-06-15T16:26:44.735Z" }, + { url = "https://files.pythonhosted.org/packages/f6/ab/9e17272fd4dac8df3b83c4fbe52b998a1c9d89a843c8c35ff29b74ff7364/sqlalchemy-2.0.51-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0f6bcad487aee1c638d707235682fc96f741de00663619881ab235400d03289e", size = 3230929, upload-time = "2026-06-15T16:19:52.625Z" }, + { url = "https://files.pythonhosted.org/packages/02/3c/52f408ea701781caee975606beccc48845f2aee8711ac29843d612c0306c/sqlalchemy-2.0.51-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:39a76529db6305693d8d4affa58ad5b5e2e18edd62daea628b29b97930b3513d", size = 3252888, upload-time = "2026-06-15T16:26:46.454Z" }, + { url = "https://files.pythonhosted.org/packages/24/16/3efd2ee6bc4ca4693a30a1dd17a91b606cae15d517d2a4746611d9b73ce8/sqlalchemy-2.0.51-cp314-cp314-win32.whl", hash = "sha256:08a204d8b5638717c26a24df18fcf40af45a6b22e35b70b1d62f0113c2e278e8", size = 2120551, upload-time = "2026-06-15T16:23:15.629Z" }, + { url = "https://files.pythonhosted.org/packages/7b/78/55b12e70f45bccc40d9e483925c065027b3b98ea4cbbdf6f8c2546feaf6c/sqlalchemy-2.0.51-cp314-cp314-win_amd64.whl", hash = "sha256:96747bfbadb055466e5b46d572618170046b45ce5a4879167f50d70a5319a499", size = 2146318, upload-time = "2026-06-15T16:23:17.108Z" }, + { url = "https://files.pythonhosted.org/packages/21/db/a9574ed40fed418924b1b1a3e54f47ee3963053b3d3d325a0d36b41f2c08/sqlalchemy-2.0.51-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e5ea1a213be1fcd5e49d9904c3b9939211ded90bc2a64e93f4c01963474285de", size = 2178920, upload-time = "2026-06-15T15:59:56.285Z" }, + { url = "https://files.pythonhosted.org/packages/bf/90/a1bb5c7cbba76b7bc1fbd586d0a5479a7bc9c27b4a8298f22ec9423b2bb3/sqlalchemy-2.0.51-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7c6b36ed71f41942bdcd2ad2522be46bfce09d5705be5640ecf19bbc7660e4b7", size = 3566534, upload-time = "2026-06-15T15:58:35.024Z" }, + { url = "https://files.pythonhosted.org/packages/15/4b/481f1fed30e0e9e8dd24aecbb49f29eb57fe7657ece5cf06ee9b84bb97d8/sqlalchemy-2.0.51-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0c2c62877097e1a0db401fba5cb4debee33265e5b2a55c4ccb489c02c53b4f72", size = 3535844, upload-time = "2026-06-15T16:02:43.973Z" }, + { url = "https://files.pythonhosted.org/packages/02/71/0aa64aeda645510af0a43f7d9ee70932f0d1dc4263aed34c50ee891d9df3/sqlalchemy-2.0.51-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0378d055e9e8cd6ce4d8dff683bdd3d7d413533c4ee51d67a2b1e0f9eacc0f23", size = 3475355, upload-time = "2026-06-15T15:58:36.592Z" }, + { url = "https://files.pythonhosted.org/packages/05/db/6061db32316446135a3abae5f308d144ab988a34234726042da3e58b1c63/sqlalchemy-2.0.51-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6e46fc36029eff666391e0531e5387b62ce6c4f1d8e50b3fb3099eaca1b42522", size = 3486591, upload-time = "2026-06-15T16:02:45.346Z" }, + { url = "https://files.pythonhosted.org/packages/0d/c9/f14fdf71bb8957e0c7e39db69bbdf12b5c80f4ef775fdfa127bf4e0d6760/sqlalchemy-2.0.51-cp314-cp314t-win32.whl", hash = "sha256:9161cfc9efce70d1715f47d6ff40f79c6778c00d53be4fbc09d70301e4b83ba7", size = 2151313, upload-time = "2026-06-15T16:03:39.127Z" }, + { url = "https://files.pythonhosted.org/packages/6a/c6/673e618e6f4f297e126d9b56ea2f6478708f6c1af4e3223835c22e2c3697/sqlalchemy-2.0.51-cp314-cp314t-win_amd64.whl", hash = "sha256:159bb6ba32059f57ad7375a8f50d844dd2f19d14954ecf820cd33e20debd46b2", size = 2186280, upload-time = "2026-06-15T16:03:40.569Z" }, + { url = "https://files.pythonhosted.org/packages/e2/22/dbf013a12ec759e54a34a119e9e217435b3f71b2dd5c61a7ade0a25dae87/sqlalchemy-2.0.51-py3-none-any.whl", hash = "sha256:bb024d8b621d0be75f4f44ecc7c950450026e76d66dc8f791bb5331d7fed59d5", size = 1944334, upload-time = "2026-06-15T16:09:22.418Z" }, ] [[package]] name = "stack-data" version = "0.6.3" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" } +source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "asttokens" }, { name = "executing" }, { name = "pure-eval" }, ] -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/28/e3/55dcc2cfbc3ca9c29519eb6884dd1415ecb53b0e934862d3559ddcb7e20b/stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9", size = 44707, upload-time = "2023-09-30T13:58:05.479Z" } +sdist = { url = "https://files.pythonhosted.org/packages/28/e3/55dcc2cfbc3ca9c29519eb6884dd1415ecb53b0e934862d3559ddcb7e20b/stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9", size = 44707, upload-time = "2023-09-30T13:58:05.479Z" } wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695", size = 24521, upload-time = "2023-09-30T13:58:03.53Z" }, + { url = "https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695", size = 24521, upload-time = "2023-09-30T13:58:03.53Z" }, ] [[package]] name = "starlette" -version = "1.3.1" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" } +version = "0.47.3" +source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/eb/e3/7c1dc7381d9f8ab7d854328ebfa884e62cb3f3d8549ddfd37c7814f42afa/starlette-1.3.1.tar.gz", hash = "sha256:05d0213193f2fbaae60e2ecb593b4add4262ad4e46536b54abe36f11a71724e0", size = 2703240, upload-time = "2026-06-12T09:23:11.602Z" } +sdist = { url = "https://files.pythonhosted.org/packages/15/b9/cc3017f9a9c9b6e27c5106cc10cc7904653c3eec0729793aec10479dd669/starlette-0.47.3.tar.gz", hash = "sha256:6bc94f839cc176c4858894f1f8908f0ab79dfec1a6b8402f6da9be26ebea52e9", size = 2584144, upload-time = "2025-08-24T13:36:42.122Z" } wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ec/bb/2799cc2ede3ed41131f8975621e7213dfc7ef4acbbaadfa440f32500c370/starlette-1.3.1-py3-none-any.whl", hash = "sha256:c7372aae11c3c3f26a42df7bd626cec2f47d03483d261d369516a615a53714c6", size = 73632, upload-time = "2026-06-12T09:23:10.017Z" }, -] - -[[package]] -name = "terminado" -version = "0.18.1" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" } -dependencies = [ - { name = "ptyprocess", marker = "os_name != 'nt'" }, - { name = "pywinpty", marker = "os_name == 'nt'" }, - { name = "tornado" }, -] -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/8a/11/965c6fd8e5cc254f1fe142d547387da17a8ebfd75a3455f637c663fb38a0/terminado-0.18.1.tar.gz", hash = "sha256:de09f2c4b85de4765f7714688fff57d3e75bad1f909b589fde880460c753fd2e", size = 32701, upload-time = "2024-03-12T14:34:39.026Z" } -wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/6a/9e/2064975477fdc887e47ad42157e214526dcad8f317a948dee17e1659a62f/terminado-0.18.1-py3-none-any.whl", hash = "sha256:a4468e1b37bb318f8a86514f65814e1afc977cf29b3992a4500d9dd305dcceb0", size = 14154, upload-time = "2024-03-12T14:34:36.569Z" }, -] - -[[package]] -name = "tinycss2" -version = "1.5.1" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" } -dependencies = [ - { name = "webencodings" }, -] -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a3/ae/2ca4913e5c0f09781d75482874c3a95db9105462a92ddd303c7d285d3df2/tinycss2-1.5.1.tar.gz", hash = "sha256:d339d2b616ba90ccce58da8495a78f46e55d4d25f9fd71dfd526f07e7d53f957", size = 88195, upload-time = "2025-11-23T10:29:10.082Z" } -wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/60/45/c7b5c3168458db837e8ceab06dc77824e18202679d0463f0e8f002143a97/tinycss2-1.5.1-py3-none-any.whl", hash = "sha256:3415ba0f5839c062696996998176c4a3751d18b7edaaeeb658c9ce21ec150661", size = 28404, upload-time = "2025-11-23T10:29:08.676Z" }, + { url = "https://files.pythonhosted.org/packages/ce/fd/901cfa59aaa5b30a99e16876f11abe38b59a1a2c51ffb3d7142bb6089069/starlette-0.47.3-py3-none-any.whl", hash = "sha256:89c0778ca62a76b826101e7c709e70680a1699ca7da6b44d38eb0a7e61fe4b51", size = 72991, upload-time = "2025-08-24T13:36:40.887Z" }, ] [[package]] name = "tornado" version = "6.5.7" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" } -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/64/24/95ec527ad67b76d59299e5465b3935d05e4294b7e0290a3924b7487df30b/tornado-6.5.7.tar.gz", hash = "sha256:66c513a76cda70d53907bc27cf1447557699c2e95aa48ba27a442ff61c3ddfc2", size = 519252, upload-time = "2026-06-08T17:34:51.232Z" } +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/64/24/95ec527ad67b76d59299e5465b3935d05e4294b7e0290a3924b7487df30b/tornado-6.5.7.tar.gz", hash = "sha256:66c513a76cda70d53907bc27cf1447557699c2e95aa48ba27a442ff61c3ddfc2", size = 519252, upload-time = "2026-06-08T17:34:51.232Z" } wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/02/dc/c7043cab6fed8ae159fc1923ce829ada35c4dbd797d408a43858ffaf9639/tornado-6.5.7-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:148b2eb15c2c765a50796172c1e499649b35f30d2e3c3d3e15913cfa56bfb163", size = 448543, upload-time = "2026-06-08T17:34:38.052Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/92/4f/090b1431e5a43df696feceffc268c5383cc079ecb5f08ce58f917109aafe/tornado-6.5.7-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:9da38de27f1da3b78a966f0dae12b5a1ea9afe72ca805d84ff06508272ddf100", size = 446707, upload-time = "2026-06-08T17:34:39.594Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/37/d8/ef374952fd5da67d4463122c2b8e5a96536ec10b4b339254c6dcde81d01c/tornado-6.5.7-cp39-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8d759e71906ee783f8867b93bf26a265743da4c1e2f4a018464c1ba019862972", size = 449774, upload-time = "2026-06-08T17:34:41.204Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/35/37/d434c73f4c6e014b745b9b37085f34f40c022f007efff3d7fe65991899f3/tornado-6.5.7-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a46347a18f23fb92b396beebe0fb78f61dda0cc302445202c16203d8a18848b", size = 450745, upload-time = "2026-06-08T17:34:42.531Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b6/2b/56b9aff361d7f1ab728a805ec7d7ea835f8807afa9f5cc690ea0e630efb9/tornado-6.5.7-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7778b30bef919231265e91c69963ce0f49a1e9c07ac900bbe75b19ce2575ba92", size = 450578, upload-time = "2026-06-08T17:34:43.787Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/02/30/a7444fb23aa76860a14198fab96ac79f1866b0a6e19e26c4381b0938e50f/tornado-6.5.7-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e726f0c75da7726eec023aa62751ff8878bd2737e34fbdd33b1ae5897d2200f5", size = 449985, upload-time = "2026-06-08T17:34:45.326Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/5c/42/5f0e56c01e8d9d36f4e23f367b85ae6cae0c1ecddd5e6977d8388ad27488/tornado-6.5.7-cp39-abi3-win32.whl", hash = "sha256:f8de3bf12d3efdd0cbe7c8887868198f8a91415e3f29fcf258d9b8eb7b1d9ae4", size = 451047, upload-time = "2026-06-08T17:34:46.784Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c9/a4/b393076ffb21b469eec5b328a0534cf03a3b90bfc6b1f09507cdd075d938/tornado-6.5.7-cp39-abi3-win_amd64.whl", hash = "sha256:de942f843533a039ef9fa3d9c88c7cd8a7c94553fb5ad0154270989b3d99a2c4", size = 451485, upload-time = "2026-06-08T17:34:48.248Z" }, - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/71/2e/7b1c769803121b809112cf9a00681c472eae1d80e32d7ec0e0bd61d0d0e1/tornado-6.5.7-cp39-abi3-win_arm64.whl", hash = "sha256:ff934fce95643af5f11efdae618eaa73d469dc588641e5c8d19295a0c65c4796", size = 450506, upload-time = "2026-06-08T17:34:49.702Z" }, + { url = "https://files.pythonhosted.org/packages/02/dc/c7043cab6fed8ae159fc1923ce829ada35c4dbd797d408a43858ffaf9639/tornado-6.5.7-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:148b2eb15c2c765a50796172c1e499649b35f30d2e3c3d3e15913cfa56bfb163", size = 448543, upload-time = "2026-06-08T17:34:38.052Z" }, + { url = "https://files.pythonhosted.org/packages/92/4f/090b1431e5a43df696feceffc268c5383cc079ecb5f08ce58f917109aafe/tornado-6.5.7-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:9da38de27f1da3b78a966f0dae12b5a1ea9afe72ca805d84ff06508272ddf100", size = 446707, upload-time = "2026-06-08T17:34:39.594Z" }, + { url = "https://files.pythonhosted.org/packages/37/d8/ef374952fd5da67d4463122c2b8e5a96536ec10b4b339254c6dcde81d01c/tornado-6.5.7-cp39-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8d759e71906ee783f8867b93bf26a265743da4c1e2f4a018464c1ba019862972", size = 449774, upload-time = "2026-06-08T17:34:41.204Z" }, + { url = "https://files.pythonhosted.org/packages/35/37/d434c73f4c6e014b745b9b37085f34f40c022f007efff3d7fe65991899f3/tornado-6.5.7-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a46347a18f23fb92b396beebe0fb78f61dda0cc302445202c16203d8a18848b", size = 450745, upload-time = "2026-06-08T17:34:42.531Z" }, + { url = "https://files.pythonhosted.org/packages/b6/2b/56b9aff361d7f1ab728a805ec7d7ea835f8807afa9f5cc690ea0e630efb9/tornado-6.5.7-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7778b30bef919231265e91c69963ce0f49a1e9c07ac900bbe75b19ce2575ba92", size = 450578, upload-time = "2026-06-08T17:34:43.787Z" }, + { url = "https://files.pythonhosted.org/packages/02/30/a7444fb23aa76860a14198fab96ac79f1866b0a6e19e26c4381b0938e50f/tornado-6.5.7-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e726f0c75da7726eec023aa62751ff8878bd2737e34fbdd33b1ae5897d2200f5", size = 449985, upload-time = "2026-06-08T17:34:45.326Z" }, + { url = "https://files.pythonhosted.org/packages/5c/42/5f0e56c01e8d9d36f4e23f367b85ae6cae0c1ecddd5e6977d8388ad27488/tornado-6.5.7-cp39-abi3-win32.whl", hash = "sha256:f8de3bf12d3efdd0cbe7c8887868198f8a91415e3f29fcf258d9b8eb7b1d9ae4", size = 451047, upload-time = "2026-06-08T17:34:46.784Z" }, + { url = "https://files.pythonhosted.org/packages/c9/a4/b393076ffb21b469eec5b328a0534cf03a3b90bfc6b1f09507cdd075d938/tornado-6.5.7-cp39-abi3-win_amd64.whl", hash = "sha256:de942f843533a039ef9fa3d9c88c7cd8a7c94553fb5ad0154270989b3d99a2c4", size = 451485, upload-time = "2026-06-08T17:34:48.248Z" }, + { url = "https://files.pythonhosted.org/packages/71/2e/7b1c769803121b809112cf9a00681c472eae1d80e32d7ec0e0bd61d0d0e1/tornado-6.5.7-cp39-abi3-win_arm64.whl", hash = "sha256:ff934fce95643af5f11efdae618eaa73d469dc588641e5c8d19295a0c65c4796", size = 450506, upload-time = "2026-06-08T17:34:49.702Z" }, ] [[package]] name = "traitlets" version = "5.15.1" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" } -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/57/a9/a2584b8313b89f94869ddb3c4074617a691de1812a614d2d50e32ca5a7a6/traitlets-5.15.1.tar.gz", hash = "sha256:7b1c07854fe25acb39e009bae49f11b79ff6cbb2f27999104e9110e7a6b53722", size = 163344, upload-time = "2026-06-03T12:26:06.181Z" } +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/a9/a2584b8313b89f94869ddb3c4074617a691de1812a614d2d50e32ca5a7a6/traitlets-5.15.1.tar.gz", hash = "sha256:7b1c07854fe25acb39e009bae49f11b79ff6cbb2f27999104e9110e7a6b53722", size = 163344, upload-time = "2026-06-03T12:26:06.181Z" } wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/96/8d/1080ee4c231f361b6ce4470d556c8c435b67c7e0753aaa641497ee92f88b/traitlets-5.15.1-py3-none-any.whl", hash = "sha256:770a53705f84b81ac107e83a1b3328ff2dae16094d8fc3cfc004e4b22dfd8e92", size = 85858, upload-time = "2026-06-03T12:26:04.395Z" }, + { url = "https://files.pythonhosted.org/packages/96/8d/1080ee4c231f361b6ce4470d556c8c435b67c7e0753aaa641497ee92f88b/traitlets-5.15.1-py3-none-any.whl", hash = "sha256:770a53705f84b81ac107e83a1b3328ff2dae16094d8fc3cfc004e4b22dfd8e92", size = 85858, upload-time = "2026-06-03T12:26:04.395Z" }, ] [[package]] name = "typing-extensions" version = "4.16.0" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" } -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" } +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" } wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, + { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, ] [[package]] name = "typing-inspection" version = "0.4.2" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" } +source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, -] - -[[package]] -name = "tzdata" -version = "2026.3" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" } -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/92/ff/5a28bdfd8c3ebec42564ac7d0e54ca3db65044a9314a97f9564fa7a1e926/tzdata-2026.3.tar.gz", hash = "sha256:4a1518b8993086a7982523e071643f3c0e5f213e75b21318e78bcabfff9d1415", size = 198674, upload-time = "2026-07-10T08:50:37.887Z" } -wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e5/6d/b53b99a9f2766d095985947a5782f1702cabb129a34f7a802d7197af832f/tzdata-2026.3-py2.py3-none-any.whl", hash = "sha256:dc096730c87af6cab1b171c9d532be840741ff5d459015e7f6947bd7d7e54931", size = 348168, upload-time = "2026-07-10T08:50:36.46Z" }, -] - -[[package]] -name = "uri-template" -version = "1.3.0" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" } -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/31/c7/0336f2bd0bcbada6ccef7aaa25e443c118a704f828a0620c6fa0207c1b64/uri-template-1.3.0.tar.gz", hash = "sha256:0e00f8eb65e18c7de20d595a14336e9f337ead580c70934141624b6d1ffdacc7" } -wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e7/00/3fca040d7cf8a32776d3d81a00c8ee7457e00f80c649f1e4a863c8321ae9/uri_template-1.3.0-py3-none-any.whl", hash = "sha256:a44a133ea12d44a0c0f06d7d42a52d71282e77e2f937d8abd5655b8d56fc1363" }, + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, ] [[package]] name = "urllib3" version = "2.7.0" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" } -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, + { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, ] [[package]] name = "uvicorn" -version = "0.51.0" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" } +version = "0.35.0" +source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "h11" }, ] -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a2/65/b7c6c443ccc58678c91e1e973bbe2a878591538655d6e1d47f24ba1c51f3/uvicorn-0.51.0.tar.gz", hash = "sha256:f6f4b69b657c312f516dd2d268ab9ae6f254b11e4bac504f37b2ab58b24dd0b0", size = 94412, upload-time = "2026-07-08T10:59:05.962Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/42/e0e305207bb88c6b8d3061399c6a961ffe5fbb7e2aa63c9234df7259e9cd/uvicorn-0.35.0.tar.gz", hash = "sha256:bc662f087f7cf2ce11a1d7fd70b90c9f98ef2e2831556dd078d131b96cc94a01", size = 78473, upload-time = "2025-06-28T16:15:46.058Z" } wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/45/ec/dbb7e5a6b91f86bfb9eb7d2988a2730907b6a729875b949c7f022e8b88fa/uvicorn-0.51.0-py3-none-any.whl", hash = "sha256:5d38af6cd620f2ae3849fb44fd4879e0890aa1febe8d47eb355fb45d93fe6a5b", size = 73219, upload-time = "2026-07-08T10:59:04.44Z" }, + { url = "https://files.pythonhosted.org/packages/d2/e2/dc81b1bd1dcfe91735810265e9d26bc8ec5da45b4c0f6237e286819194c3/uvicorn-0.35.0-py3-none-any.whl", hash = "sha256:197535216b25ff9b785e29a0b79199f55222193d47f820816e7da751e9bc8d4a", size = 66406, upload-time = "2025-06-28T16:15:44.816Z" }, +] + +[package.optional-dependencies] +standard = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "httptools" }, + { name = "python-dotenv" }, + { name = "pyyaml" }, + { name = "uvloop", marker = "platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'" }, + { name = "watchfiles" }, + { name = "websockets" }, +] + +[[package]] +name = "uvloop" +version = "0.22.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/06/f0/18d39dbd1971d6d62c4629cc7fa67f74821b0dc1f5a77af43719de7936a7/uvloop-0.22.1.tar.gz", hash = "sha256:6c84bae345b9147082b17371e3dd5d42775bddce91f885499017f4607fdaf39f", size = 2443250, upload-time = "2025-10-16T22:17:19.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/ff/7f72e8170be527b4977b033239a83a68d5c881cc4775fca255c677f7ac5d/uvloop-0.22.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fe94b4564e865d968414598eea1a6de60adba0c040ba4ed05ac1300de402cd42", size = 1359936, upload-time = "2025-10-16T22:16:29.436Z" }, + { url = "https://files.pythonhosted.org/packages/c3/c6/e5d433f88fd54d81ef4be58b2b7b0cea13c442454a1db703a1eea0db1a59/uvloop-0.22.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:51eb9bd88391483410daad430813d982010f9c9c89512321f5b60e2cddbdddd6", size = 752769, upload-time = "2025-10-16T22:16:30.493Z" }, + { url = "https://files.pythonhosted.org/packages/24/68/a6ac446820273e71aa762fa21cdcc09861edd3536ff47c5cd3b7afb10eeb/uvloop-0.22.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:700e674a166ca5778255e0e1dc4e9d79ab2acc57b9171b79e65feba7184b3370", size = 4317413, upload-time = "2025-10-16T22:16:31.644Z" }, + { url = "https://files.pythonhosted.org/packages/5f/6f/e62b4dfc7ad6518e7eff2516f680d02a0f6eb62c0c212e152ca708a0085e/uvloop-0.22.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b5b1ac819a3f946d3b2ee07f09149578ae76066d70b44df3fa990add49a82e4", size = 4426307, upload-time = "2025-10-16T22:16:32.917Z" }, + { url = "https://files.pythonhosted.org/packages/90/60/97362554ac21e20e81bcef1150cb2a7e4ffdaf8ea1e5b2e8bf7a053caa18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e047cc068570bac9866237739607d1313b9253c3051ad84738cbb095be0537b2", size = 4131970, upload-time = "2025-10-16T22:16:34.015Z" }, + { url = "https://files.pythonhosted.org/packages/99/39/6b3f7d234ba3964c428a6e40006340f53ba37993f46ed6e111c6e9141d18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:512fec6815e2dd45161054592441ef76c830eddaad55c8aa30952e6fe1ed07c0", size = 4296343, upload-time = "2025-10-16T22:16:35.149Z" }, + { url = "https://files.pythonhosted.org/packages/89/8c/182a2a593195bfd39842ea68ebc084e20c850806117213f5a299dfc513d9/uvloop-0.22.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:561577354eb94200d75aca23fbde86ee11be36b00e52a4eaf8f50fb0c86b7705", size = 1358611, upload-time = "2025-10-16T22:16:36.833Z" }, + { url = "https://files.pythonhosted.org/packages/d2/14/e301ee96a6dc95224b6f1162cd3312f6d1217be3907b79173b06785f2fe7/uvloop-0.22.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cdf5192ab3e674ca26da2eada35b288d2fa49fdd0f357a19f0e7c4e7d5077c8", size = 751811, upload-time = "2025-10-16T22:16:38.275Z" }, + { url = "https://files.pythonhosted.org/packages/b7/02/654426ce265ac19e2980bfd9ea6590ca96a56f10c76e63801a2df01c0486/uvloop-0.22.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e2ea3d6190a2968f4a14a23019d3b16870dd2190cd69c8180f7c632d21de68d", size = 4288562, upload-time = "2025-10-16T22:16:39.375Z" }, + { url = "https://files.pythonhosted.org/packages/15/c0/0be24758891ef825f2065cd5db8741aaddabe3e248ee6acc5e8a80f04005/uvloop-0.22.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0530a5fbad9c9e4ee3f2b33b148c6a64d47bbad8000ea63704fa8260f4cf728e", size = 4366890, upload-time = "2025-10-16T22:16:40.547Z" }, + { url = "https://files.pythonhosted.org/packages/d2/53/8369e5219a5855869bcee5f4d317f6da0e2c669aecf0ef7d371e3d084449/uvloop-0.22.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bc5ef13bbc10b5335792360623cc378d52d7e62c2de64660616478c32cd0598e", size = 4119472, upload-time = "2025-10-16T22:16:41.694Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ba/d69adbe699b768f6b29a5eec7b47dd610bd17a69de51b251126a801369ea/uvloop-0.22.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1f38ec5e3f18c8a10ded09742f7fb8de0108796eb673f30ce7762ce1b8550cad", size = 4239051, upload-time = "2025-10-16T22:16:43.224Z" }, + { url = "https://files.pythonhosted.org/packages/90/cd/b62bdeaa429758aee8de8b00ac0dd26593a9de93d302bff3d21439e9791d/uvloop-0.22.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3879b88423ec7e97cd4eba2a443aa26ed4e59b45e6b76aabf13fe2f27023a142", size = 1362067, upload-time = "2025-10-16T22:16:44.503Z" }, + { url = "https://files.pythonhosted.org/packages/0d/f8/a132124dfda0777e489ca86732e85e69afcd1ff7686647000050ba670689/uvloop-0.22.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4baa86acedf1d62115c1dc6ad1e17134476688f08c6efd8a2ab076e815665c74", size = 752423, upload-time = "2025-10-16T22:16:45.968Z" }, + { url = "https://files.pythonhosted.org/packages/a3/94/94af78c156f88da4b3a733773ad5ba0b164393e357cc4bd0ab2e2677a7d6/uvloop-0.22.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:297c27d8003520596236bdb2335e6b3f649480bd09e00d1e3a99144b691d2a35", size = 4272437, upload-time = "2025-10-16T22:16:47.451Z" }, + { url = "https://files.pythonhosted.org/packages/b5/35/60249e9fd07b32c665192cec7af29e06c7cd96fa1d08b84f012a56a0b38e/uvloop-0.22.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1955d5a1dd43198244d47664a5858082a3239766a839b2102a269aaff7a4e25", size = 4292101, upload-time = "2025-10-16T22:16:49.318Z" }, + { url = "https://files.pythonhosted.org/packages/02/62/67d382dfcb25d0a98ce73c11ed1a6fba5037a1a1d533dcbb7cab033a2636/uvloop-0.22.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b31dc2fccbd42adc73bc4e7cdbae4fc5086cf378979e53ca5d0301838c5682c6", size = 4114158, upload-time = "2025-10-16T22:16:50.517Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/f1171b4a882a5d13c8b7576f348acfe6074d72eaf52cccef752f748d4a9f/uvloop-0.22.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:93f617675b2d03af4e72a5333ef89450dfaa5321303ede6e67ba9c9d26878079", size = 4177360, upload-time = "2025-10-16T22:16:52.646Z" }, + { url = "https://files.pythonhosted.org/packages/79/7b/b01414f31546caf0919da80ad57cbfe24c56b151d12af68cee1b04922ca8/uvloop-0.22.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:37554f70528f60cad66945b885eb01f1bb514f132d92b6eeed1c90fd54ed6289", size = 1454790, upload-time = "2025-10-16T22:16:54.355Z" }, + { url = "https://files.pythonhosted.org/packages/d4/31/0bb232318dd838cad3fa8fb0c68c8b40e1145b32025581975e18b11fab40/uvloop-0.22.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b76324e2dc033a0b2f435f33eb88ff9913c156ef78e153fb210e03c13da746b3", size = 796783, upload-time = "2025-10-16T22:16:55.906Z" }, + { url = "https://files.pythonhosted.org/packages/42/38/c9b09f3271a7a723a5de69f8e237ab8e7803183131bc57c890db0b6bb872/uvloop-0.22.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:badb4d8e58ee08dad957002027830d5c3b06aea446a6a3744483c2b3b745345c", size = 4647548, upload-time = "2025-10-16T22:16:57.008Z" }, + { url = "https://files.pythonhosted.org/packages/c1/37/945b4ca0ac27e3dc4952642d4c900edd030b3da6c9634875af6e13ae80e5/uvloop-0.22.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b91328c72635f6f9e0282e4a57da7470c7350ab1c9f48546c0f2866205349d21", size = 4467065, upload-time = "2025-10-16T22:16:58.206Z" }, + { url = "https://files.pythonhosted.org/packages/97/cc/48d232f33d60e2e2e0b42f4e73455b146b76ebe216487e862700457fbf3c/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:daf620c2995d193449393d6c62131b3fbd40a63bf7b307a1527856ace637fe88", size = 4328384, upload-time = "2025-10-16T22:16:59.36Z" }, + { url = "https://files.pythonhosted.org/packages/e4/16/c1fd27e9549f3c4baf1dc9c20c456cd2f822dbf8de9f463824b0c0357e06/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6cde23eeda1a25c75b2e07d39970f3374105d5eafbaab2a4482be82f272d5a5e", size = 4296730, upload-time = "2025-10-16T22:17:00.744Z" }, +] + +[[package]] +name = "watchfiles" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cd/41/5e1a4bb12aac5f1493fa1bdc11154eca3b258ca4eba65d39c473fe19d8e9/watchfiles-1.2.0.tar.gz", hash = "sha256:c995fba777f1ea992f090f9236e9284cf7a5d1a0130dd5a3d82c598cacd76838", size = 108252, upload-time = "2026-05-18T04:32:04.251Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b8/2f/e42c992d2afda3108ea1c02acecc991b9f31d05c14adc2a7cee9ee211fc4/watchfiles-1.2.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:bc13eb17538be00c874699dc0abe4ee2bc8d50bb1166a6b9e175ef3fd7eb8f26", size = 400115, upload-time = "2026-05-18T04:32:02.06Z" }, + { url = "https://files.pythonhosted.org/packages/5f/8f/6af2ea19065c91d8b0ea3516fdfc8c0d349f407e8e9fbf4e5a17360de8ad/watchfiles-1.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2d95ddc1eb6914154253d239089900813f6a767e174b8e6a50e7fdacb7e4236c", size = 393659, upload-time = "2026-05-18T04:30:50.951Z" }, + { url = "https://files.pythonhosted.org/packages/13/01/b32a967c56fb3e3e5be3db52c3d3b87fa4513aa367d8ed1ad96d42952e5f/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f70d8b291ef6e88d19b1f297a6905ddb978888d9272b0d05e6f53309856bcfc", size = 453207, upload-time = "2026-05-18T04:31:04.231Z" }, + { url = "https://files.pythonhosted.org/packages/04/98/97557a812180338cb1abd32e1cffcc4588f59b5f23e0cb006b2ba95ba64a/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:56d8641cf834c2836922899105bd3ce3d0dfc69291d52edf0b4d0436829b34c0", size = 459273, upload-time = "2026-05-18T04:31:50.377Z" }, + { url = "https://files.pythonhosted.org/packages/e8/a8/b4b08dcb7653b8087c6586f7ce649505900e866bbcfe40dc9587af02e686/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2581a94056e55d7d0a31a823ea92bf73749c489ca2285bfdc0fbe6b2bb49d50c", size = 489927, upload-time = "2026-05-18T04:31:42.485Z" }, + { url = "https://files.pythonhosted.org/packages/50/94/3dceea03545d2e5ddfd839f0ddd5e1cecbf1697b5a428d5ba11cef6af95d/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:41bc1199f7523b3f82843c88cbb979180c949caef0342cf90968f178e5d49b01", size = 570476, upload-time = "2026-05-18T04:31:03.071Z" }, + { url = "https://files.pythonhosted.org/packages/cc/f2/d39a5450c3532092b91f81d274360e613c2371bc874a89c7a1a3c5e8d138/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7571e4464cb6e434958f867f7f730b8ab0b75e3f8e5eac0499168486ab3c33a8", size = 465650, upload-time = "2026-05-18T04:30:12.701Z" }, + { url = "https://files.pythonhosted.org/packages/22/24/ed72f68cbc1333ca9b9f2200aa048bb6658ae41709bc1caad4310f4bdffd/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e53a384f76b631c3ae5334ce6a52f0baa3a911eb94a4eac7f160079868b716d5", size = 456398, upload-time = "2026-05-18T04:30:13.784Z" }, + { url = "https://files.pythonhosted.org/packages/0d/64/982ef4a4e5bab5b6e5b6becc8cd5e732f6130a78b855f0abec6439a9a135/watchfiles-1.2.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:d20029a60a71a052a24c4db7673bc4de39ab89adbaccbfb5d67987c5d73f424d", size = 465140, upload-time = "2026-05-18T04:31:52.111Z" }, + { url = "https://files.pythonhosted.org/packages/a0/0c/95282abf4ed680b6096010bcfc30c5fa7a041fc5aa5a2ad17a2cc6c75bba/watchfiles-1.2.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:2cb93af48550faf1cea04c303107c8b75833de7013e57ce27d3b8d21d8d0f58c", size = 630259, upload-time = "2026-05-18T04:31:25.676Z" }, + { url = "https://files.pythonhosted.org/packages/30/45/607c1de1530c4bdcf2cf1d1ecc2505ddba5d96bd43ba9f2b0e79876f850f/watchfiles-1.2.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2995c176de7692b86a2e4c58d9ec718f753150a979cb4a754e2b4ffa38e70906", size = 659859, upload-time = "2026-05-18T04:30:24.333Z" }, + { url = "https://files.pythonhosted.org/packages/fa/08/d9e2e0f9e8e6791d33aefc694ad7eefa7f901f63caff84a81ded38692f9c/watchfiles-1.2.0-cp312-cp312-win32.whl", hash = "sha256:7a2cffd17d27d2ecbb310c2b1d8174f222a5495b1a721894afa88ec11e25b898", size = 275480, upload-time = "2026-05-18T04:30:31.307Z" }, + { url = "https://files.pythonhosted.org/packages/1c/e6/9d42569c0102645cc8cea5d8c7d8a1e9d4ada2cb7f05f75e554b8aa2202a/watchfiles-1.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:f155b3a1b2a5fc89cdc70d47ee5d54e3b75e88efa34982028a35daef9ba00379", size = 288718, upload-time = "2026-05-18T04:32:10.745Z" }, + { url = "https://files.pythonhosted.org/packages/0a/26/88e0dc6ee3898169d7fa22bb6a69cabf2502d2ee25cb8c876d1262d204f8/watchfiles-1.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:8fa585ede612ee9f9e91b18bebf9ba11b9ae29a4e3a0d0cf6fca3e382133f0d5", size = 281026, upload-time = "2026-05-18T04:30:22.23Z" }, + { url = "https://files.pythonhosted.org/packages/d1/4d/70a7feced9f87e2ff26dba42667290f41694fc64646c67261fbb8cab5d5c/watchfiles-1.2.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:01ea8d66f0693b9b60a6541c8d10263091ca9a9060d242f3c1f3143f9aad2c98", size = 399730, upload-time = "2026-05-18T04:31:38.162Z" }, + { url = "https://files.pythonhosted.org/packages/31/3a/0da302f2307aee316922806ebd5726c542cbd787c938271cf14a074c7daf/watchfiles-1.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7ba0480b9a74af058f43b337e937a451e109295c420916d68ad24e3dc02f5e44", size = 392842, upload-time = "2026-05-18T04:30:27.051Z" }, + { url = "https://files.pythonhosted.org/packages/db/ef/d5bdb705c224dbc256aa0c1ec47bf4e61ec52558f2afb44a71a1fe4d7015/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f34e26a19f91f710c08e0183429f0d1d15df734e6bc78c31e77b9ea9c433658", size = 452989, upload-time = "2026-05-18T04:31:11.945Z" }, + { url = "https://files.pythonhosted.org/packages/71/29/5495f2c1661949ef7a35e4d71111d129cfe7606414a26887a919d0a55406/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b4e77f6a55f858504069abd35d336a637555c09bca453dde1ee1e5ada8a6a1fb", size = 458978, upload-time = "2026-05-18T04:30:52.606Z" }, + { url = "https://files.pythonhosted.org/packages/d5/8c/7f9c07c433811c2fffd93e13fdfb7135de9aab5f2ae41be08960fa0047dc/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0cb4d80e212f116474a545c21c912b445f16bb0cef9e6a73a498164223e14e2f", size = 490248, upload-time = "2026-05-18T04:31:36.003Z" }, + { url = "https://files.pythonhosted.org/packages/3c/11/d93632febc52fbc21be90231bb7c17fd5387f46c9076fd40a5f9c2ae6910/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b974946a10af379d425e2eef5b62f5c6ebeaccf91d45eaad6f5b27ecd4f91aa0", size = 571847, upload-time = "2026-05-18T04:31:10.862Z" }, + { url = "https://files.pythonhosted.org/packages/55/b4/383173e73aabb07ad1d9c7aa859d95437ac46a6d6a1e11005facda0c9d19/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:86bc13c25a8d1fcd70b51d0ce7c9b65e90de5666fcbfd3e34957cc73ee19aeb5", size = 465974, upload-time = "2026-05-18T04:30:17.006Z" }, + { url = "https://files.pythonhosted.org/packages/a7/6c/89b1a230a78f57c52dd8893adb1f92f94411721b6ec12596c56d98c74356/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca148d73dea36c9763aaa351e4d7a51780ec1584217c45276f4fe8239c768b71", size = 454782, upload-time = "2026-05-18T04:30:35.656Z" }, + { url = "https://files.pythonhosted.org/packages/24/62/1732118367cfff0a9fce3bf62ff4bfded09ef5df21d9d446b858b3f70a96/watchfiles-1.2.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:c525543d91961c6955b2636b308569e84a1d1c5f5f2932041ab9ef46422f43e3", size = 465182, upload-time = "2026-05-18T04:30:20.846Z" }, + { url = "https://files.pythonhosted.org/packages/28/96/716f7e5f51339bf22963f3345f9f27d7f3b30e2eadc597e257c881dd3c53/watchfiles-1.2.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:a204794696ffb8f9b10fba6f7cb5216d42f3b2b71860ccac6b6e42f5f10973b0", size = 629841, upload-time = "2026-05-18T04:31:05.397Z" }, + { url = "https://files.pythonhosted.org/packages/4c/fe/c40783950fd771ccf66ab3ec2722d188a9af1c7f96c6e811f36e40c6e03f/watchfiles-1.2.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:10d86db20695afe7997ac9e1717637d6714a8d0220458c33f3d2061f54cec427", size = 658028, upload-time = "2026-05-18T04:31:48.22Z" }, + { url = "https://files.pythonhosted.org/packages/71/72/4508db1856d1d87fcbb3b63f4839bab1b5682cb0e8d224d122263c09654a/watchfiles-1.2.0-cp313-cp313-win32.whl", hash = "sha256:eb283ee99e21ad6443c8cdb06ac5b34b1308c329cbdf03fa02b445363714c799", size = 275183, upload-time = "2026-05-18T04:30:59.57Z" }, + { url = "https://files.pythonhosted.org/packages/f9/36/14b76ca57652e5cc5fd1c11f32a261292c08a0d19a00351013c2549cbfb2/watchfiles-1.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:a0f27f01bee51861392bb6b7c4fdb290b27d1eb194e9e28788d68102a0e898d9", size = 288059, upload-time = "2026-05-18T04:32:07.937Z" }, + { url = "https://files.pythonhosted.org/packages/1b/8d/0a85e395398d8d20fadfe5c5d32c726eee17a519e78fb356f2cf7531bffe/watchfiles-1.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:3651aa7058595e9cfb75d35dd5ada2bf9f48a5b8a0f3562821d3e210c507e077", size = 280186, upload-time = "2026-05-18T04:31:54.484Z" }, + { url = "https://files.pythonhosted.org/packages/37/68/36db056f1fdcc5f07302f56e631774d6835bcd6fa3ace402304621d5f9e5/watchfiles-1.2.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:faea288b6f0ab1902ef08f4ca6de005dccf856c4e0c4f21b8c5fce02d90a1b08", size = 399031, upload-time = "2026-05-18T04:30:44.576Z" }, + { url = "https://files.pythonhosted.org/packages/c1/64/01a9d6f66a82a5c101ce939274106cc72759d62427e153f01edd2b9f87c2/watchfiles-1.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:01859b11fd9fbca670f4d5da00fbac282cfea9bd67a2125d8b2833a3b5617ea9", size = 391205, upload-time = "2026-05-18T04:30:25.413Z" }, + { url = "https://files.pythonhosted.org/packages/84/2c/0a44fe058cb4bb7b8ede6b6670698bbb7c0400740e378d00022189b7b31d/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fff610d7bb2256a317bb1e96f0d7862c7aa8076733ee5df0fd41bbe76a24a4f4", size = 451892, upload-time = "2026-05-18T04:32:14.005Z" }, + { url = "https://files.pythonhosted.org/packages/67/a1/351e0d56cd35e6488b5c8b4fb11a809a5bc923e8fe8fed9faf8920be0c89/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b141a4891c995a039cd89e9a49e62df1dc8a559a5d1a6e4c7106d16c12777a55", size = 458867, upload-time = "2026-05-18T04:31:22.279Z" }, + { url = "https://files.pythonhosted.org/packages/d5/7d/9d09605187f1b838998624049fcf8bf47b73c1a3b76901fcac1782f62277/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f22943b7770483f6ea0721c6b11d022947a98eb0acae14694de034f4d0d38925", size = 490217, upload-time = "2026-05-18T04:31:43.657Z" }, + { url = "https://files.pythonhosted.org/packages/60/5d/a17a16eccb182f04188cd308ec24b1a71a9b5c4e7098269cf35d9fa56d02/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1bc6195825b7dcd217968bb1f801a60fd4c16e8eeab5bedc7fe917d7d5995ab4", size = 571458, upload-time = "2026-05-18T04:32:11.875Z" }, + { url = "https://files.pythonhosted.org/packages/d3/3d/4dd457062083ab1938e5dfd45032eb425cee2ac817287ca8ff4356183e5d/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d4a4b147f5dca2a5d325a06a832fb43f345751adfbc63204aec30e0d9ca965a2", size = 464707, upload-time = "2026-05-18T04:30:43.492Z" }, + { url = "https://files.pythonhosted.org/packages/c6/71/ea8c57b128f5383de74d0c7d2d9c57ad7c9a65a930c451bd25d524b295b7/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4543579a9bdb0c9560039b4ffddbdb39545707659fbc430ce4c10f3f68d557f9", size = 454663, upload-time = "2026-05-18T04:30:16.061Z" }, + { url = "https://files.pythonhosted.org/packages/53/fd/2e812bf938406d7db351f0703ddd3fc6c061cf30d96153a77bc79a943a44/watchfiles-1.2.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:20aa0e708b920bde876a4aa82dc7dd6ebea228a63a67cda6632c2fc87b787efa", size = 463537, upload-time = "2026-05-18T04:31:44.9Z" }, + { url = "https://files.pythonhosted.org/packages/86/56/d17a7f1dd1bc3035f1072694a551301272f1739c2d8e319c927cb9e29b38/watchfiles-1.2.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:d413349d565dab74297f2a63e84a097936be69bf8f3b3801f27f380e32040f44", size = 629194, upload-time = "2026-05-18T04:31:14.141Z" }, + { url = "https://files.pythonhosted.org/packages/be/06/f1ff66bf5cae50aa4062779a0ecd0bbaf15e466195719074078947d9a17d/watchfiles-1.2.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:f28b2725eb8cce327b9b3ab02415c853011dc55c95832fe90de6bc56f5315f72", size = 656194, upload-time = "2026-05-18T04:31:47.14Z" }, + { url = "https://files.pythonhosted.org/packages/e7/54/a9c7ea9a82a4ac65e7004c0a03920b5cdd2f9c3b678757d9cd425aa51d53/watchfiles-1.2.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:b8c8358484d5fa12ef34f05b7f4168eaf1932f408725ff6d023c33ec17bd79d4", size = 400205, upload-time = "2026-05-18T04:32:05.153Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5d/c9ab3534374a4a67450696905d6ef16a04405448b8dc52bd752ae50423d4/watchfiles-1.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f04b092229ad2c50126dd3c922c8822e51e605993764a33058d4a791ab42281", size = 392508, upload-time = "2026-05-18T04:30:54.849Z" }, + { url = "https://files.pythonhosted.org/packages/26/ca/1ad30103535cf0cecd7b993e8d50edc5351b1820e38f2d22e3df58962feb/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7a7ce236284f002a156f70add88efe5c70879cccbb658be0822c54b1306fc09d", size = 452448, upload-time = "2026-05-18T04:30:53.727Z" }, + { url = "https://files.pythonhosted.org/packages/37/a1/ceee2cdf2afbd715fa07758d39c9859513eae411b23196f7fd039e5feedd/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b9909cc2b48468b575eefa944919e1fe8a36c5849d5c7c168f80a8c1db69398e", size = 459605, upload-time = "2026-05-18T04:30:23.312Z" }, + { url = "https://files.pythonhosted.org/packages/e8/f6/421e30fd1cb3907a84ed92ab3f1983e37ba2dca015e9a894a048418417a2/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a37faaed405c67e28e6be45a1fa4f206ef5a2860f27c237db9fa30704c38242", size = 490757, upload-time = "2026-05-18T04:30:47.358Z" }, + { url = "https://files.pythonhosted.org/packages/41/b0/55ed1b97ed08be7bba6f9a541cac15f2a858e1d74d2b07b6da70a82aab00/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9649193aa27bd9ff2e80ff29bfaa93085496c7a3a377592823cc58b77ee88add", size = 568672, upload-time = "2026-05-18T04:30:38.915Z" }, + { url = "https://files.pythonhosted.org/packages/d1/cf/d8ae8a80dd7bafab395ea7681c10237311bbf34d37704a8c744e7cf31fc7/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e4ff8e37f99cf1da89e255e07c9c4b37c214038c4283707bdec308cb1b0ea1f", size = 464197, upload-time = "2026-05-18T04:30:09.914Z" }, + { url = "https://files.pythonhosted.org/packages/7c/8a/3076c496ca8dafe0e8cd03fcebdfc47be4b1174b4e5b24ff6e396e6b3af2/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:054dc20fd2e3132b4c3883b4a00d72fd6e1f56fdaf89fccd12e8057d74cd74d7", size = 453181, upload-time = "2026-05-18T04:30:14.829Z" }, + { url = "https://files.pythonhosted.org/packages/e5/10/9745e17c98e7b8a86454df0a3c7b5686bd650383f1e9f26e4ebcbd6cc0c0/watchfiles-1.2.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:e140ed30ebde76796b686e67c182cff10ea2fbab186fafd1560f74bb5a473a6e", size = 465109, upload-time = "2026-05-18T04:30:28.123Z" }, + { url = "https://files.pythonhosted.org/packages/8f/95/8ef4a95481d3e0cb52d62a06fa6e972e81424be2d9698b91a2fecca9904c/watchfiles-1.2.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:bb7e52ecf68ba46d22df23467b87cffeb2146908aa523ebfe803019618cfda06", size = 630653, upload-time = "2026-05-18T04:31:49.304Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e4/3b3bf36b0f829b50c6ebcb8d031583863c59f923d6a6af3d485e470d0fac/watchfiles-1.2.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:23282a321c8baf9b3a3c4afff673f9fe65eb7fdc2338d765ccad9d3d1916a5ba", size = 657838, upload-time = "2026-05-18T04:31:06.497Z" }, + { url = "https://files.pythonhosted.org/packages/21/b1/6cbbb50c1f3002ab568777d44aa21206dfb8807a840990c4037523b51812/watchfiles-1.2.0-cp314-cp314-win32.whl", hash = "sha256:c0db965c5f79aa49fe672d297cf1febc5ad149b658594944f49a54a2b96270a7", size = 275108, upload-time = "2026-05-18T04:30:06.891Z" }, + { url = "https://files.pythonhosted.org/packages/92/45/190ce6db8dcb4536682cf75d3889ff1a27182a58cb519d343cb6d9ea63d8/watchfiles-1.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:71283b39fd17e5408eb123bd37aeecfd9d54c81fc184421943208aadb879d103", size = 288441, upload-time = "2026-05-18T04:32:12.901Z" }, + { url = "https://files.pythonhosted.org/packages/74/0d/3eae1c2313ab08378431d907c3f8095ecca00f3eda33111cf4f0f2591799/watchfiles-1.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:c5c19526f4e54a00f2666a6c0e9e40d582c09e865055ea7378bf0009aab857b3", size = 280684, upload-time = "2026-05-18T04:31:26.902Z" }, + { url = "https://files.pythonhosted.org/packages/b1/75/fb64e6c25d6b5ca636d03df34ffb1c6e9873303e76d27967e045f8df088f/watchfiles-1.2.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:d73a585accffa5ae39c17264c36ec3166d2fad7000c780f5ef83b2722afb9dd2", size = 398857, upload-time = "2026-05-18T04:32:17.108Z" }, + { url = "https://files.pythonhosted.org/packages/73/4e/9f7adf01754cbf81843722ccfec169d8f26c69778281a302855cecd2ee08/watchfiles-1.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ae99b14c5f21e026e0e9d96f40e07d8570ebee6cafd9d8fc318354606daa7a28", size = 392413, upload-time = "2026-05-18T04:31:07.911Z" }, + { url = "https://files.pythonhosted.org/packages/47/c8/bec626bcc2d69f44b9acb24ce7d60ed7b16b73628eea747fcbd169d8edda/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4429f3b105524a10b72c3a819b091c495d2811d419c1e1e8df773a5a5974f831", size = 452409, upload-time = "2026-05-18T04:31:20.142Z" }, + { url = "https://files.pythonhosted.org/packages/00/b7/b6362068e81e7c556d155a34c35d40ac3ef42d747b06d7f6e5bf58e359c2/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:43d818978d06062d9b22c4fab2ebe44cf5213d42dc8e62bda8c2760cfa2eeb33", size = 458827, upload-time = "2026-05-18T04:32:06.219Z" }, + { url = "https://files.pythonhosted.org/packages/67/f8/9a813fa42afb1e0b4625e75f0479826644d3ee8dc287e093799bc01f390c/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b9f732dc58b2dbe69e464ccf8fff7a03b0dd0be439da4c0720d3558527d3d6b4", size = 490104, upload-time = "2026-05-18T04:31:56.034Z" }, + { url = "https://files.pythonhosted.org/packages/2f/bf/27dfb6094ca4c9aad21298b5525b6c53cb36121ee454331d05161e58d130/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8f200104103feb097de4cab8fe4f5dd18a2026934c7dea98c55a2f5fd6d5a33b", size = 571360, upload-time = "2026-05-18T04:31:57.133Z" }, + { url = "https://files.pythonhosted.org/packages/fb/39/44a096d67270ea93df91d33877dbe91fbda3aa4f8ec2edf799d93eda8736/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:63ac26eefbf4af1741247d6fb68b11c49a25b2f7413fbd318a83a12aaa9cf666", size = 464644, upload-time = "2026-05-18T04:30:57.33Z" }, + { url = "https://files.pythonhosted.org/packages/0e/80/c7472203bad6268e3ef1ad260739704847898938ad7ea8b63a5131f46b50/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c4997d4e4a55f0d02b6cde327322daf3a0400e5df6c6b15948994bf72497925", size = 454771, upload-time = "2026-05-18T04:30:48.736Z" }, + { url = "https://files.pythonhosted.org/packages/51/cf/3b10b268b4b7f0fc26e9debb5eef1998b515887840f444cd3ec80c688755/watchfiles-1.2.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:4c887eba18b7945ac73067a8b4a66f21cd46c2539b2bc68588f7be6c7eb6d26b", size = 463494, upload-time = "2026-05-18T04:31:33.826Z" }, + { url = "https://files.pythonhosted.org/packages/3d/3e/a4302545cd589262a0dc7d140e86f7688eba3f9c72776c27f7e23b8864c4/watchfiles-1.2.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:3416ff151bb6b5a8d8d11664974fbef4d9305b9b2957839ab5a270468fd8df30", size = 629383, upload-time = "2026-05-18T04:31:15.596Z" }, + { url = "https://files.pythonhosted.org/packages/db/99/d5649df0a9a410d45b7c882304d0b790903ac9b6e8f2cfd12114e0c6b9f2/watchfiles-1.2.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:0e831a271c035d89789cffc386b6aa1375f39f1cd25eb7ca0997e4970d152fc5", size = 656093, upload-time = "2026-05-18T04:31:58.707Z" }, + { url = "https://files.pythonhosted.org/packages/92/b9/362702539275019a54dd2e94511b31a9b89c5f9e6a21966de7eb692549fc/watchfiles-1.2.0-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:37a6721cdf3f65dbb13aa9503510ccb4451603ac837e44d265d7992a597e1374", size = 400109, upload-time = "2026-05-18T04:31:16.879Z" }, + { url = "https://files.pythonhosted.org/packages/8f/75/71d5ba62db781e5587bded1d944c675374bc4aa37ff33d5018d98e8b6538/watchfiles-1.2.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2b37d10b5a63bd4d87e18472d80fa525bd670586fae62e5dd580452764879b65", size = 392167, upload-time = "2026-05-18T04:31:28.058Z" }, + { url = "https://files.pythonhosted.org/packages/3c/01/c66dd95d0423fe30d31820e2d1d5bda773764131bbb6ac0cb1cf303ac328/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a105bc2283f67e8fbec74253ec2d94925de92ed72c0393f1206bf326b7b7b69", size = 452372, upload-time = "2026-05-18T04:31:00.836Z" }, + { url = "https://files.pythonhosted.org/packages/91/15/2fe99557e72f85627c6a8eed50d889e8d101623e060a22ad75b875cb932d/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5327989a465505f05cfe06f04fa9d0c2fd5432bb243e10e6f012b1bdca3c8579", size = 459596, upload-time = "2026-05-18T04:31:34.96Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/d4acfa0023367428ed48351b3b9b267893037b6cadae55620c61c24bcfd4/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ecb47f183a8025b2aa18b546725c3657e542112ae9c0613a2af79b4fa8d04ad7", size = 490869, upload-time = "2026-05-18T04:31:59.923Z" }, + { url = "https://files.pythonhosted.org/packages/a4/5f/3164cbdce06c9fb95c4f7b9e2f9760b5e2797af43a9ecc317ef42a23a278/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8520a4ab0e37f770afc34459c4f8f7019e153f9124dc101c15538365875d1ab2", size = 571641, upload-time = "2026-05-18T04:32:00.948Z" }, + { url = "https://files.pythonhosted.org/packages/41/e6/85d3731c55e65cd7690f3f803d24c139588aaf863e4bf2148fe7a7fa1a19/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:71cd71740ed2c15211ebb237ced4e39a1cdf6f80566e5fe95428da1626f4fde6", size = 464444, upload-time = "2026-05-18T04:30:34.298Z" }, + { url = "https://files.pythonhosted.org/packages/f4/7d/562641012b8b09872742c3b8adf9629ec479fd78f8d68ae4a0c13da8add6/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f88af53d6ddaf72179ef613ddc905e6f4785f712b49b80b3bef9f3525e6194b4", size = 453593, upload-time = "2026-05-18T04:31:23.464Z" }, + { url = "https://files.pythonhosted.org/packages/56/fe/cb8ef3d6f929d14158fdaaad9925985b7310abc9384dcd4d82dd0016fb59/watchfiles-1.2.0-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:cee9d5efd929efdac5f7e58f72b3376f676b64050a91c5b99a7094c5b2317488", size = 465096, upload-time = "2026-05-18T04:31:30.384Z" }, + { url = "https://files.pythonhosted.org/packages/25/91/80908e835e100527a9267147b08c0eee1fa6ab0ffec15edc04d1d44885f7/watchfiles-1.2.0-cp315-cp315-musllinux_1_1_aarch64.whl", hash = "sha256:b718bf356bbc15e559bd8ef41782b573b8ae0e3f177ab244b440568d7ea02cfb", size = 630638, upload-time = "2026-05-18T04:30:49.89Z" }, + { url = "https://files.pythonhosted.org/packages/46/4b/95ab2f256bb4af3cb2eb23b9317bda984ee6e0f11733a5c004a6c95b06e3/watchfiles-1.2.0-cp315-cp315-musllinux_1_1_x86_64.whl", hash = "sha256:922c0e019fe68b3ae392965a766b02a71ba1168c932cebc3733cd52c5fe5b377", size = 657684, upload-time = "2026-05-18T04:31:32.027Z" }, ] [[package]] name = "wcwidth" version = "0.8.2" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" } -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/34/74/c6428f875774288bec1396f5bfcbc2d925700a4dad61727fd5f2b12f249d/wcwidth-0.8.2.tar.gz", hash = "sha256:91fbef97204b96a3d4d421609b80340b760cf33e26da123ff243d76b1fda8dda", size = 1466253, upload-time = "2026-06-29T18:11:11.601Z" } +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/34/74/c6428f875774288bec1396f5bfcbc2d925700a4dad61727fd5f2b12f249d/wcwidth-0.8.2.tar.gz", hash = "sha256:91fbef97204b96a3d4d421609b80340b760cf33e26da123ff243d76b1fda8dda", size = 1466253, upload-time = "2026-06-29T18:11:11.601Z" } wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/96/42/3e5985a0a7e57de470b320c6d6a1a67c844f6737a587f3d44dd13d1819e7/wcwidth-0.8.2-py3-none-any.whl", hash = "sha256:d63947694a0539a1d51e01eda7caf800c291020e6cdd7e28ad7b14dd33ad4f85", size = 323166, upload-time = "2026-06-29T18:11:09.888Z" }, + { url = "https://files.pythonhosted.org/packages/96/42/3e5985a0a7e57de470b320c6d6a1a67c844f6737a587f3d44dd13d1819e7/wcwidth-0.8.2-py3-none-any.whl", hash = "sha256:d63947694a0539a1d51e01eda7caf800c291020e6cdd7e28ad7b14dd33ad4f85", size = 323166, upload-time = "2026-06-29T18:11:09.888Z" }, ] [[package]] -name = "webcolors" -version = "25.10.0" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" } -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/1d/7a/eb316761ec35664ea5174709a68bbd3389de60d4a1ebab8808bfc264ed67/webcolors-25.10.0.tar.gz", hash = "sha256:62abae86504f66d0f6364c2a8520de4a0c47b80c03fc3a5f1815fedbef7c19bf", size = 53491, upload-time = "2025-10-31T07:51:03.977Z" } +name = "websockets" +version = "16.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/02/b9a097e1e16fee4e2fd1ec8c39f6a9c5d6257bae8fa12640caf869f54436/websockets-16.1.tar.gz", hash = "sha256:299468cbe42e2b9981134c7c51d99387d8a7bf562b00183b3eec53f882846dad", size = 182530, upload-time = "2026-07-10T06:32:57.734Z" } wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e2/cc/e097523dd85c9cf5d354f78310927f1656c422bd7b2613b2db3e3f9a0f2c/webcolors-25.10.0-py3-none-any.whl", hash = "sha256:032c727334856fc0b968f63daa252a1ac93d33db2f5267756623c210e57a4f1d", size = 14905, upload-time = "2025-10-31T07:51:01.778Z" }, -] - -[[package]] -name = "webencodings" -version = "0.5.1" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" } -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/0b/02/ae6ceac1baeda530866a85075641cec12989bd8d31af6d5ab4a3e8c92f47/webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923", size = 9721, upload-time = "2017-04-05T20:21:34.189Z" } -wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", size = 11774, upload-time = "2017-04-05T20:21:32.581Z" }, -] - -[[package]] -name = "websocket-client" -version = "1.9.0" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" } -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/2c/41/aa4bf9664e4cda14c3b39865b12251e8e7d239f4cd0e3cc1b6c2ccde25c1/websocket_client-1.9.0.tar.gz", hash = "sha256:9e813624b6eb619999a97dc7958469217c3176312b3a16a4bd1bc7e08a46ec98", size = 70576, upload-time = "2025-10-07T21:16:36.495Z" } -wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/34/db/b10e48aa8fff7407e67470363eac595018441cf32d5e1001567a7aeba5d2/websocket_client-1.9.0-py3-none-any.whl", hash = "sha256:af248a825037ef591efbf6ed20cc5faa03d3b47b9e5a2230a529eeee1c1fc3ef", size = 82616, upload-time = "2025-10-07T21:16:34.951Z" }, -] - -[[package]] -name = "win32-setctime" -version = "1.2.0" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple/" } -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b3/8f/705086c9d734d3b663af0e9bb3d4de6578d08f46b1b101c2442fd9aecaa2/win32_setctime-1.2.0.tar.gz", hash = "sha256:ae1fdf948f5640aae05c511ade119313fb6a30d7eabe25fef9764dca5873c4c0", size = 4867, upload-time = "2024-12-07T15:28:28.314Z" } -wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e1/07/c6fe3ad3e685340704d314d765b7912993bcb8dc198f0e7a89382d37974b/win32_setctime-1.2.0-py3-none-any.whl", hash = "sha256:95d644c4e708aba81dc3704a116d8cbc974d70b3bdb8be1d150e36be6e9d1390", size = 4083, upload-time = "2024-12-07T15:28:26.465Z" }, + { url = "https://files.pythonhosted.org/packages/a1/52/748c014f07f4e0e170c8932de7e647a1511d5ab3049cd978797136aee577/websockets-16.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b6aa3f7ad345cf3862c21f4fbf2ef5e14d911348476c2845e137c091fe3a3f0b", size = 179798, upload-time = "2026-07-10T06:31:09.664Z" }, + { url = "https://files.pythonhosted.org/packages/8b/5e/2a2e64d977d084e49d37c187c26c056daaff41965be7300cd5dbde6f8b07/websockets-16.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b43fcfb521ac2f34ba80b7b8ea16303e4ad82dd8af667bf40839ad3a5d37b164", size = 177478, upload-time = "2026-07-10T06:31:11.072Z" }, + { url = "https://files.pythonhosted.org/packages/aa/12/5b85b4e75d697e548a94962ce5c036b05dd21cb9545759d555c5586422fc/websockets-16.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2bd3e12cd9afbe2baedae0b1eeade8ba64329b60fe2f9abdc966bd10fd2c2ef5", size = 177746, upload-time = "2026-07-10T06:31:12.386Z" }, + { url = "https://files.pythonhosted.org/packages/9d/62/79b1c8f0cee0da648b4899e1c5b0dbd3aa59846985136a54854db6827ab4/websockets-16.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:35f41979c8623df9bd30d949d82010a8fda5c56ff12cd8508a5b7272b6d4b53a", size = 187345, upload-time = "2026-07-10T06:31:13.754Z" }, + { url = "https://files.pythonhosted.org/packages/25/34/b7c5c52c2f24280e1c017acb7ad491a566750a5cceca7f3cf999373bba21/websockets-16.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a24d1f35aef07d794a16c853c688e74956c50239bec37b4f2de080056046419b", size = 188581, upload-time = "2026-07-10T06:31:15.075Z" }, + { url = "https://files.pythonhosted.org/packages/bc/37/604193bebcbeffe96fdf795960b83a15d600880c64dc17ec9c31c5b3427d/websockets-16.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0c64c024ddf7a35331b21fcddb562a039c275d2c82e8c2d12939e7da23997270", size = 191362, upload-time = "2026-07-10T06:31:16.395Z" }, + { url = "https://files.pythonhosted.org/packages/a5/b4/5ee27575b367d7110d4d13945e2a9de067ec84dc71e54b87f01e38550d9a/websockets-16.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c3e99757f5baafe20fc598e202ea6f5b0b265186ad38d0a17bd8beca16296955", size = 189216, upload-time = "2026-07-10T06:31:17.776Z" }, + { url = "https://files.pythonhosted.org/packages/7e/22/3e2dcc78d85fc5d9d814895ce6d07d0dfacc0f6aaa1d151f2b8c8d772299/websockets-16.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:353f3bc6e058ac1ccab4b3588e8598837a8c04cfc8351233e6d523be675d844c", size = 187971, upload-time = "2026-07-10T06:31:19.152Z" }, + { url = "https://files.pythonhosted.org/packages/9e/2f/cd271717b93d5ee19626cb5e38a85baab745c86e33db7c31a3ac729b31b8/websockets-16.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0352f5b38b40e857b6428d468fa21dbb4dd4a567d933c26d9831b4efe1b92f43", size = 185381, upload-time = "2026-07-10T06:31:20.665Z" }, + { url = "https://files.pythonhosted.org/packages/78/91/6ad6f2f1426317b5001bd490534208c7360636b35bac1dec2e0c22bfc40e/websockets-16.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:70bd789afab579602968c39f21cb925466505f3edff22f0ae852bca54978a4f9", size = 188015, upload-time = "2026-07-10T06:31:22.024Z" }, + { url = "https://files.pythonhosted.org/packages/c7/6d/533733132ab4c07540efd4a8f0b9a435d3a5059b2f26cc476ace1abf7f45/websockets-16.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:d0fb4b46f121eccd539353baebd1083a8767a9a351109453d1d1caecd1ba40c2", size = 186619, upload-time = "2026-07-10T06:31:23.376Z" }, + { url = "https://files.pythonhosted.org/packages/08/73/16c059f3d73b3331eba10793704afa4faa9939234fb08ef7dca35794e8f0/websockets-16.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c14b6634af01541e4efe2954fd8f263386f7aa6d37c01e55dd8109fd17661452", size = 188497, upload-time = "2026-07-10T06:31:25.024Z" }, + { url = "https://files.pythonhosted.org/packages/4d/89/9a8fae7dd2acdcfb1a8844c29fe42b518a04b64fce38a0923b6290e452f1/websockets-16.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:a58532c49a851bcb481e58c1be23b315c17fe2fbbed509d75aeea12f543d2c15", size = 186051, upload-time = "2026-07-10T06:31:26.291Z" }, + { url = "https://files.pythonhosted.org/packages/f6/40/b240c7dd6a0e0c59c1f68377cc3015263521080c327c15f5e753c1f6d378/websockets-16.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4e969170c3b08e1d8dabd990fef1fa702c4233aeaabec33f871806e444f6a0e4", size = 187029, upload-time = "2026-07-10T06:31:27.605Z" }, + { url = "https://files.pythonhosted.org/packages/50/35/524e3fac40e47d6fdcf6c4b2c95ef1bc8a97e01593c90eff86621df7b716/websockets-16.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ff9b000064b88787ba9f7a3cb2af2b68a658ca5aad76458a46469e7124b678a0", size = 187308, upload-time = "2026-07-10T06:31:28.927Z" }, + { url = "https://files.pythonhosted.org/packages/00/13/56840cf62c8859af6ba22b9529da937332468c80f32b598753e8a66d3990/websockets-16.1-cp312-cp312-win32.whl", hash = "sha256:b9f5d83f80f4d7c4bba6d97f3755ac05850c784dce0fd2ab371c4e41172f53ff", size = 180161, upload-time = "2026-07-10T06:31:30.316Z" }, + { url = "https://files.pythonhosted.org/packages/d6/ff/87eb9eb44cb62424a8d729834f2b0515a47e2669fabec29820268f4d50a1/websockets-16.1-cp312-cp312-win_amd64.whl", hash = "sha256:6852c9f653966c16109d3b6f31181fd734f7914927e3f0fa1117af7a18c9aa21", size = 180462, upload-time = "2026-07-10T06:31:31.708Z" }, + { url = "https://files.pythonhosted.org/packages/d9/63/df158b155420b566f025e75613424ad9649a24bcb0e9f259321ab3d58bea/websockets-16.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:b0232ed141cec3df2af5a3959a071c51f40036336b0d37e17faf9ef52fc73e47", size = 179791, upload-time = "2026-07-10T06:31:33.108Z" }, + { url = "https://files.pythonhosted.org/packages/74/cf/00fe9414dfeafa6fe54eae9f5716c8c8e9ac59d192be3b893c096d395846/websockets-16.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a71b73d143991714144e159f767b698f03c4a70b8a65ae1733b650cff488045b", size = 177472, upload-time = "2026-07-10T06:31:34.522Z" }, + { url = "https://files.pythonhosted.org/packages/8b/76/b10633424d40681b4e892ffd08ca5226322b2426e62d4ab71eae484c3a32/websockets-16.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:187323204c3b2fc465e8fc2609e60437c521790cb9c1acb49c4c452a33e57f37", size = 177737, upload-time = "2026-07-10T06:31:35.964Z" }, + { url = "https://files.pythonhosted.org/packages/dc/61/d3bb03b2229bb1afd72008742d586cf1ea240dce64dd48c71c8c7fd3294c/websockets-16.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9dba74233c8c3ce368850818c98354dad2570f57231b3fd3bd00d7aa57628881", size = 187403, upload-time = "2026-07-10T06:31:37.496Z" }, + { url = "https://files.pythonhosted.org/packages/26/16/cc2e80478f688fc3c39c67dc1fac6a0783858058914ebc2489917462cb42/websockets-16.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:63339bc8c63c86a463177775cb7c677691f5bcfac7b3b2f01b286d42acd41600", size = 188639, upload-time = "2026-07-10T06:31:38.86Z" }, + { url = "https://files.pythonhosted.org/packages/15/d6/ad87b2507e57de1cbf897a56c963f2925962ed5e85fbe06aaa83ced27acd/websockets-16.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:23e545ea8ae4263e37cdfd4e22a217f519e48e432728bc461185bbf585f38a83", size = 190078, upload-time = "2026-07-10T06:31:40.218Z" }, + { url = "https://files.pythonhosted.org/packages/9e/1a/5b37b3fd335d5811f29fc829f2646a3e6d1463a4bf09c3100708684c766e/websockets-16.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2237081454846fb40403a80ba86d82e2038b9c45865ab96af0abe7d002a91045", size = 189267, upload-time = "2026-07-10T06:31:41.523Z" }, + { url = "https://files.pythonhosted.org/packages/42/98/06afc33e9450d4230f94c664db78875d90f5f6a5fb77f0bc6ec15ae74e1c/websockets-16.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5f5218de1ed047385ca53744caba9435d65f75d008364970a3fae95a05812cf9", size = 188022, upload-time = "2026-07-10T06:31:42.838Z" }, + { url = "https://files.pythonhosted.org/packages/8c/bf/42fef5d5887c18cf2d148b02debf56cecb9cfbffc68027cde9b12c8f432c/websockets-16.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:75c98e3920039d0edff03b74478ada504b7ce3a1bc406db2cabfca84320f7baf", size = 185435, upload-time = "2026-07-10T06:31:44.219Z" }, + { url = "https://files.pythonhosted.org/packages/a0/9b/8021c133add5fe40ed40312553a6cd1408c069d7efe3444ad483d4973ed3/websockets-16.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1facd189d8190af30487a55b4c3688484dd50801628a3b5b2ccd26db08e67057", size = 188080, upload-time = "2026-07-10T06:31:45.986Z" }, + { url = "https://files.pythonhosted.org/packages/69/54/1e37384f395eaa127383aab15c1c45e200890a7d7b99db5c312233d193e0/websockets-16.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:cc0c6a6eef613c7da32d4fb068f82ef834b58134f6a16b54e6c1e5bf9529ab3d", size = 186678, upload-time = "2026-07-10T06:31:47.449Z" }, + { url = "https://files.pythonhosted.org/packages/68/79/1caeacab5bc2081e4519288d248bc8bd2de30652e6eaa94be6be09a1fe5b/websockets-16.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:ad9411eded8988b879be6038206698bf7106c85a78f642c004485bcb95be17eb", size = 188554, upload-time = "2026-07-10T06:31:48.886Z" }, + { url = "https://files.pythonhosted.org/packages/ee/83/b3dca5fad71487b726e31cb0acf56f226792c1cc34e6ab18cbf146bd2d74/websockets-16.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:cd68f0914f3b64694895bc5e9b14e8b447e41d7bf5ffaf989bb8dcb5e2dfdce7", size = 186109, upload-time = "2026-07-10T06:31:50.508Z" }, + { url = "https://files.pythonhosted.org/packages/5b/0b/8f246c3712f07f207b52ea5fb47f3b2b66fafec7303162644c74aed51c6a/websockets-16.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fef2debfe7f7ebdda12176f26166f95b7af17af05ba06150fcf889032e0213e9", size = 187061, upload-time = "2026-07-10T06:31:51.861Z" }, + { url = "https://files.pythonhosted.org/packages/47/eb/27d6c92a01696b6495386af4fc941d7d0a13f2eab2bf9c336111d7321491/websockets-16.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a3cd6c9b798218798f4bb7b2e71c38f0e744bb94ca537b13376f88019d46384d", size = 187347, upload-time = "2026-07-10T06:31:53.246Z" }, + { url = "https://files.pythonhosted.org/packages/6b/d5/eeee439921f55d5eaeabcea18d0f7ce32cdc39cb8fc1e185431a094c5c7b/websockets-16.1-cp313-cp313-win32.whl", hash = "sha256:84c170c6869633536921e4474b1cce7254c0c9b0053ef5725f966cee47e718e4", size = 180149, upload-time = "2026-07-10T06:31:55.058Z" }, + { url = "https://files.pythonhosted.org/packages/a3/03/971e98d4a4864cf263f9e94c5b2b7c9a9b7682d77bfbba4e732c55ee85a9/websockets-16.1-cp313-cp313-win_amd64.whl", hash = "sha256:bef52d327d70fa75dad93ee61ea2cb1d1489aca9f35c188833563f5a3b4df0a5", size = 180458, upload-time = "2026-07-10T06:31:56.767Z" }, + { url = "https://files.pythonhosted.org/packages/8d/e6/da1dc11507f8118145a81c751fe0c77e5e1c11b8554496addb39389e2dc2/websockets-16.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:f881fca0a45dd6789939bd6637cd98169b92f1c3fdc78262f2cb9ec2cb1f324e", size = 179833, upload-time = "2026-07-10T06:31:58.19Z" }, + { url = "https://files.pythonhosted.org/packages/6e/ac/c0d46f62e31e232487b2c123bc3cfd9a4e45684ca7dc0c37f0987f29baae/websockets-16.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:30c379d5b207d3a7f0ba4c2e4602a895b0bcc63fb5f5371a4ae7fbddb03b672b", size = 177524, upload-time = "2026-07-10T06:31:59.563Z" }, + { url = "https://files.pythonhosted.org/packages/4a/33/abd966074b34a51e4f134e0aaed80f5a4a0a35163ea5ac58a1bc5a076d23/websockets-16.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:98ab58a4faa72b46da0127ccc1931dcbfc0985b0778892300a092185910c4cbe", size = 177743, upload-time = "2026-07-10T06:32:00.959Z" }, + { url = "https://files.pythonhosted.org/packages/ea/30/646e47b8a8dff04e227bdab512e6dde60663a647eeac7bbd6edddd92bbc5/websockets-16.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8e9c4e369fc181b2d41a99e01477215cecdc8546a39f7d41a59cc0a7065a0b09", size = 187474, upload-time = "2026-07-10T06:32:02.54Z" }, + { url = "https://files.pythonhosted.org/packages/d2/72/890ab9d77494af93ea65268230bfbc0a90ba789401ed7a44356a44785644/websockets-16.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0704df094b2d5fa7f6f410925a594c2a5c9a09167731a76292e5410934208209", size = 188717, upload-time = "2026-07-10T06:32:04.156Z" }, + { url = "https://files.pythonhosted.org/packages/d5/aa/baedbbaa6bf9ed6029617ed5e8976535bd805f483ca9b3484e7ad9ee08bf/websockets-16.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b22b1f4950f6ab7126623329c3b47b3b90a14c05db517f2db2a026ad6c928352", size = 190090, upload-time = "2026-07-10T06:32:05.822Z" }, + { url = "https://files.pythonhosted.org/packages/52/4f/d813ec94e18002571ef4959d87a630eff6e01b72a51bcb0832b75ae8c51a/websockets-16.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1ae4a686a662964a6671069f84f7f908cc3475e782227726b0c622c715962105", size = 189320, upload-time = "2026-07-10T06:32:07.223Z" }, + { url = "https://files.pythonhosted.org/packages/b8/3c/8ec52a6662f3df64090fba28cd521d405d54759268d8e820477037e8c80d/websockets-16.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:856bdd638f8277f86465057bfdd4da097c73058fb0f9d2bd5baea29e2bf2d367", size = 188068, upload-time = "2026-07-10T06:32:08.586Z" }, + { url = "https://files.pythonhosted.org/packages/96/7f/f0ae6042b14f86fa5f996c6563ea4cf107adc036ccbedc9d4f418d0095f9/websockets-16.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9003a1fde1c21a322a3ca3fa0c4bda8c639da81dbc925162766086643b05ba87", size = 185493, upload-time = "2026-07-10T06:32:09.968Z" }, + { url = "https://files.pythonhosted.org/packages/89/ad/5ffc53af9939c49fd653d147fa5b8f78ced1f6bce6c49a7446860945b0ce/websockets-16.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:39e947b1f5fdab045174306e3916785bf3ed537648acc1549827c08c33b10953", size = 188141, upload-time = "2026-07-10T06:32:11.434Z" }, + { url = "https://files.pythonhosted.org/packages/67/62/729206c0ee577a4db8eae6dd06e0eef725a1287c6df11b2ef831d003df31/websockets-16.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:5dd0e666b5931c0509cf65714686a1c5126771e663a79ac5d40da4f58b1f9502", size = 186653, upload-time = "2026-07-10T06:32:12.845Z" }, + { url = "https://files.pythonhosted.org/packages/1b/86/e8806a99ec4589914f255e6b658853fe537bf359c05e6ba5762ad9c27917/websockets-16.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:a0285df7925657ad65a65fb8dc330808bce082827538fd50ef45fa12d1fc5bca", size = 188614, upload-time = "2026-07-10T06:32:14.236Z" }, + { url = "https://files.pythonhosted.org/packages/89/38/ac554e2fc6ff0b8deeff9798b92e7abd8f99e2bd9731532e7033de208220/websockets-16.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:82d1c2cab3c133e9d059b3a5420bed9376bd30e21c185c63dda4ddadf6ddda47", size = 186165, upload-time = "2026-07-10T06:32:15.626Z" }, + { url = "https://files.pythonhosted.org/packages/6c/c5/4ef4d8e53342f94f3c49e1ae089b32c1e8b3878e15e0022c7708c647f351/websockets-16.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:c39907f1eaf11f6277def65aa02d68f30576b693d0c1ca332aafa3caa723ac6d", size = 187119, upload-time = "2026-07-10T06:32:17.114Z" }, + { url = "https://files.pythonhosted.org/packages/3a/33/4788b1dd417bd97eeb2698af3b9df6775ac656f96e9987da0419a067602f/websockets-16.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:45c5ea55446171949eb99fd34b771ceddd511ca21958d40d0197ced33159e5ee", size = 187411, upload-time = "2026-07-10T06:32:18.629Z" }, + { url = "https://files.pythonhosted.org/packages/30/38/00d37aad6dc3244ce349e2864815362e50b3cfc00cac28d216db20efe40f/websockets-16.1-cp314-cp314-win32.whl", hash = "sha256:b8ef8b1c8d6bd029a475ac432e730fba2dfd456715d26c473e2a82291024b99c", size = 179822, upload-time = "2026-07-10T06:32:20.233Z" }, + { url = "https://files.pythonhosted.org/packages/9d/37/2a8cb0eaddee5eaebda47a90a3ba0898d1ce3d866b02a4857fea17d82e5b/websockets-16.1-cp314-cp314-win_amd64.whl", hash = "sha256:7358ff21632b5d062707f73e859c824f1c3807e73d8ca25e71caca7c4cdcf145", size = 180167, upload-time = "2026-07-10T06:32:21.749Z" }, + { url = "https://files.pythonhosted.org/packages/07/5a/262ad5fcaef4198997b165060f09a63f861e76939b1786ab546ccc3f8120/websockets-16.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:d0f38f4c3e9b359e257c339c2cc1967ccaeedb102e57c1c986bdce4bf4f32268", size = 180166, upload-time = "2026-07-10T06:32:23.278Z" }, + { url = "https://files.pythonhosted.org/packages/1a/c7/36377db690f4292826e4501a6dec2801dc55fd1cf0405923b04937e478df/websockets-16.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:3c3d2cbd1602593bad49bd86fa3fbb25407d87a3b4bf8857c0ac5ac4914e1901", size = 177697, upload-time = "2026-07-10T06:32:25.164Z" }, + { url = "https://files.pythonhosted.org/packages/fa/c7/07171abce1e39799a76f473608580fe98bd43a1230f5146159622c02bccf/websockets-16.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:36069b74671e7e667f48a7484249f84c45a825a134c8b1bdc01875d0daa10d79", size = 177902, upload-time = "2026-07-10T06:32:26.564Z" }, + { url = "https://files.pythonhosted.org/packages/14/17/c831f48e250bc4749f57c00dcce73337c41cd32f6d59a64567b84e782601/websockets-16.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:587f83c2ce8a5d628e166384d77fa7f0ac69b9007d515ab442123e6615aa8da3", size = 187766, upload-time = "2026-07-10T06:32:27.981Z" }, + { url = "https://files.pythonhosted.org/packages/2c/2e/4dfe63e245b0ecfaf470cf082d25c6ce35808159135fd88c82653a6b11ab/websockets-16.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a6db7972d52bc1b66cefe2246902e256cbaebc9ba8a45eac09343d7eb6671b2", size = 188939, upload-time = "2026-07-10T06:32:29.365Z" }, + { url = "https://files.pythonhosted.org/packages/ba/e5/5faf65aebd9562f6b4bc473d24ce38cc56f84eb5f5bee66ed9b86733f93c/websockets-16.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e7d6014888a0632e1ed7a4095248bb3095232999447f2d83bfb1900987dd9ed9", size = 191081, upload-time = "2026-07-10T06:32:30.868Z" }, + { url = "https://files.pythonhosted.org/packages/49/cd/2634f2f2c0556c1aae6501ed6840019cc569dd6fdbcac6494378daea4dc0/websockets-16.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9cb074d150e4ad2a77aa8a332c2be85f3f64f2681519d2570c1225c12c9821ff", size = 189513, upload-time = "2026-07-10T06:32:32.399Z" }, + { url = "https://files.pythonhosted.org/packages/59/bb/2c700b51196104f09715b326b1f092ed25326bdf79a03e00a4842e503743/websockets-16.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d19c9067e1fe9490f974bffbc0e443b80a7674c5efb4980c429cc00771f07c5a", size = 188240, upload-time = "2026-07-10T06:32:33.897Z" }, + { url = "https://files.pythonhosted.org/packages/f1/20/86283636e499a1a357fa9441f690ba34f255e731f2fea174132b3b762b57/websockets-16.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d440ff0c6c7469ad59c0a412c383c235935b43635e89425e3f6a0c36de90c31b", size = 185955, upload-time = "2026-07-10T06:32:35.279Z" }, + { url = "https://files.pythonhosted.org/packages/91/23/d7fb734b0095d43bc7f1c9f68afd50adb4176e7e513403e8c70ad7daa4fa/websockets-16.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8613129a2533f08de24505e69a3e403cedaadae49abdb043c4d170ca71b7e4bd", size = 188491, upload-time = "2026-07-10T06:32:36.673Z" }, + { url = "https://files.pythonhosted.org/packages/6a/5e/168a192689db468405ecf3b8e4a2c18811936b0724d017ad7e6d252734f0/websockets-16.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:a5bf9c23f197b4ec88290fd5463f33db67362a1bb10f85fc2e8e7627f0ddab97", size = 186983, upload-time = "2026-07-10T06:32:38.207Z" }, + { url = "https://files.pythonhosted.org/packages/7e/9b/66795fa91ebe49019ebe4fa910282172252e37046b80e08fc52e0c365150/websockets-16.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:520b0fd0395f075febb283c76755af724ab9fd19dffa4f3bfd18cb4e622790a3", size = 188890, upload-time = "2026-07-10T06:32:39.545Z" }, + { url = "https://files.pythonhosted.org/packages/5a/32/126bbc844be5afb3613fd43211dac10a9645f4cf39741d04acaa2ec7030c/websockets-16.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:7143aa09a67e1c013be44e81a88dfe90fc6244198ab86c7edd064152cf619805", size = 186583, upload-time = "2026-07-10T06:32:41.038Z" }, + { url = "https://files.pythonhosted.org/packages/22/b9/0b5db9cbcf6e4970db4496893244a8d92e07f71a8ef27cf34b08aa02fef1/websockets-16.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:7acb811fad08e611755800d1560e395c67e11a6bd563598ea6abb319afb86938", size = 187353, upload-time = "2026-07-10T06:32:42.501Z" }, + { url = "https://files.pythonhosted.org/packages/99/2e/254b2131a10d831b76e2c18dfe7add9729c6292c674a8085bf8de01ad151/websockets-16.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c5cf88e3faa2f7931bc6baeee7599c97656a3f6ac7f831f4fccba233e141783a", size = 187784, upload-time = "2026-07-10T06:32:43.929Z" }, + { url = "https://files.pythonhosted.org/packages/21/dc/e7288aa8e3ac5a88a0924619984d663c1abf2a87d0ea98290c66fdaee0ec/websockets-16.1-cp314-cp314t-win32.whl", hash = "sha256:589f8842521c8307684ce0b40ce4ad70c5e0aa46484c6f1225a94ef4b8970341", size = 179947, upload-time = "2026-07-10T06:32:45.495Z" }, + { url = "https://files.pythonhosted.org/packages/d3/de/37edf1260ff0fbbd2f82433489c4cfbe799ac2ff21355331609879329fe6/websockets-16.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c0e0857c30bbbc2bb5c30687508f0b7ec19aa026cd9f2ff8424d0fee42dcc07", size = 180291, upload-time = "2026-07-10T06:32:47.119Z" }, + { url = "https://files.pythonhosted.org/packages/66/58/bd83247f39ddc26ffc2c24eb05087a3b749e00cb4509fc6d19daa23c8495/websockets-16.1-py3-none-any.whl", hash = "sha256:c5149dfe490ec7e5ee5dbf624c642fb725f93a5575c7f00ab594ca9eddb8dd81", size = 174031, upload-time = "2026-07-10T06:32:56.079Z" }, ]