diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 00000000..4761de95 --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,57 @@ +# ── 后端 Dockerfile ────────────────────────────────────────────────── +# 多阶段构建:builder 装依赖 → runtime 只拷贝产物,镜像更小 + +# ── 阶段 1: 构建 ── +FROM python:3.12-slim AS builder + +# 安装 uv(比 pip 快 10x) +COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv + +# ── 国内网络:走镜像源 + 拉长超时 ──────────────────────────────────────── +# 实测(2026-08-04,这台服务器):宿主机访问 pypi.org 需 8s,构建容器内默认超时 +# 会在下载大包(uvloop)时 "operation timed out" 直接失败。 +ENV UV_DEFAULT_INDEX=https://mirrors.aliyun.com/pypi/simple/ \ + UV_HTTP_TIMEOUT=180 + +# 必须与 runtime 阶段同路径 —— venv 内的 shebang/.pth 是绝对路径,跨路径拷贝会失效 +WORKDIR /app + +# 先拷贝依赖定义,利用 Docker layer cache +COPY pyproject.toml uv.lock ./ +COPY packages/common/pyproject.toml packages/common/ +COPY packages/framework/pyproject.toml packages/framework/ +COPY packages/ai_engine/pyproject.toml packages/ai_engine/ +COPY packages/app/pyproject.toml packages/app/ + +# 安装依赖(不含 dev 依赖) +RUN uv sync --frozen --no-dev --no-install-workspace + +# 拷贝源码并安装 +COPY packages/ packages/ +RUN uv sync --frozen --no-dev + +# ── 阶段 2: 运行时 ── +FROM python:3.12-slim AS runtime + +WORKDIR /app + +# 从 builder 拷贝虚拟环境和包 +COPY --from=builder /app/.venv /app/.venv +COPY --from=builder /app/packages /app/packages + +# 把 venv/bin 加入 PATH +ENV PATH="/app/.venv/bin:$PATH" + +# 默认环境变量(可被 docker-compose / .env 覆盖) +ENV WINDUP_HOST=0.0.0.0 +ENV WINDUP_PORT=8000 +ENV WINDUP_RELOAD=false + +EXPOSE 8000 + +# 健康检查 +HEALTHCHECK --interval=30s --timeout=5s --retries=3 \ + CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/docs')" || exit 1 + +# 启动命令 +CMD ["uvicorn", "windup_app.bootstrap.app:create_app", "--factory", "--host", "0.0.0.0", "--port", "8000"] diff --git a/backend/packages/app/pyproject.toml b/backend/packages/app/pyproject.toml index 43c573b0..52c4de6a 100644 --- a/backend/packages/app/pyproject.toml +++ b/backend/packages/app/pyproject.toml @@ -9,7 +9,7 @@ dependencies = [ "windup-ai-engine", "fastapi>=0.115", "uvicorn[standard]>=0.30", - "pydantic>=2.7", + "pydantic[email]>=2.7", "sqlalchemy>=2.0", "python-multipart>=0.0.9", ] diff --git a/backend/packages/app/src/windup_app/bootstrap/app.py b/backend/packages/app/src/windup_app/bootstrap/app.py index 89f7b43d..58a31182 100644 --- a/backend/packages/app/src/windup_app/bootstrap/app.py +++ b/backend/packages/app/src/windup_app/bootstrap/app.py @@ -1,15 +1,104 @@ + """FastAPI 应用工厂与装配入口。 ``create_app`` 负责创建 FastAPI 实例并挂载路由 / 中间件 / 异常处理, 是整个 web 服务的唯一装配点(composition root)。 + +``main`` 是开发启动入口:``python -m windup_app`` 或 ``windup`` 命令。 """ +import os +from contextlib import asynccontextmanager + +import windup_framework.db # noqa: F401 组装时显式触发 DB engine/session 初始化 from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware +from windup_app.server.orchestrator.executor import run_action_task, run_image_task +from windup_app.web.api.auth import router as auth_router +from windup_app.web.api.character import router as character_router +from windup_app.web.api.generation import router as generation_router from windup_app.web.api.media import router as media_router +from windup_app.web.api.project import router as project_router +from windup_app.web.handler.exception_handlers import register_exception_handlers +from windup_app.web.middleware.auth import AuthMiddleware +from windup_app.web.middleware.ratelimit import RateLimitMiddleware + + +def _env_flag(name: str) -> bool: + """把环境变量解析为真正的布尔值:仅 1/true/yes/on(忽略大小写与空白)视为 True。""" + return os.getenv(name, "").strip().lower() in {"1", "true", "yes", "on"} + + + +def _cors_origins() -> list[str]: + """允许跨域的前端来源,逗号分隔的 WINDUP_CORS_ORIGINS 覆盖。 + + 不配这个中间件的话,浏览器会把前端的**所有**请求拦在预检那一步 + (OPTIONS 返回 405、响应无 access-control-* 头),后端日志里连请求都看不到。 + 默认值覆盖本地 dev server 与 Vercel 预览域名。 + """ + raw = os.getenv("WINDUP_CORS_ORIGINS", "").strip() + if raw: + return [o.strip() for o in raw.split(",") if o.strip()] + return ["http://localhost:5173", "http://127.0.0.1:5173", + "http://localhost:3000", "http://127.0.0.1:3000"] + + +def print_banner() -> None: + """启动时打印 banner(占位实现,后续替换为正式 ASCII banner)。""" + print("windup 0.1.0 starting ...") + + +@asynccontextmanager +async def _lifespan(app: FastAPI): + """应用启动时打印 banner,关闭时无特殊处理。""" + print_banner() + yield def create_app() -> FastAPI: - app = FastAPI(title="windup", version="0.1.0") + app = FastAPI(title="windup", version="0.1.0", lifespan=_lifespan) + + # 中间件(执行顺序:请求先进 RateLimit,再进 Auth,最后到路由) + app.add_middleware(RateLimitMiddleware) + app.add_middleware(AuthMiddleware) + + # 路由 + app.include_router(auth_router) + app.add_middleware( + CORSMiddleware, + allow_origins=_cors_origins(), + allow_origin_regex=r"https://.*\.vercel\.app", + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], + ) + app.include_router(project_router) + app.include_router(character_router) app.include_router(media_router) + app.include_router(generation_router) + + # 生成后台调度器注入 app.state + app.state.run_action_task = run_action_task + app.state.run_image_task = run_image_task + + register_exception_handlers(app) return app + + +def main() -> None: + """开发启动入口:用 uvicorn 跑 ``create_app``。""" + import uvicorn + + uvicorn.run( + "windup_app.bootstrap.app:create_app", + factory=True, + host=os.getenv("WINDUP_HOST", "127.0.0.1"), + port=int(os.getenv("WINDUP_PORT", "8000")), + reload=_env_flag("WINDUP_RELOAD"), + ) + + +if __name__ == "__main__": + main() diff --git a/backend/packages/app/src/windup_app/server/user/interface.py b/backend/packages/app/src/windup_app/server/user/interface.py index 8e7fe2ec..84c22755 100644 --- a/backend/packages/app/src/windup_app/server/user/interface.py +++ b/backend/packages/app/src/windup_app/server/user/interface.py @@ -1,6 +1,6 @@ """用户领域服务抽象接口。 -API 层只依赖本模块定义的抽象,不感知具体实现(ORM / Redis / OAuth SDK)。 +API 层只依赖本模块定义的抽象,不感知具体实现(ORM / Redis / Resend)。 """ from abc import ABC, abstractmethod @@ -11,7 +11,7 @@ LoginByPasswordInput, LoginResult, RegisterInput, - User, + UserView, ) @@ -37,9 +37,10 @@ def login_by_password(self, input: LoginByPasswordInput) -> LoginResult: """ @abstractmethod - def send_verification_code(self, email: str) -> None: + def send_verification_code(self, email: str, purpose: str) -> None: """发送邮箱验证码。 + :param purpose: 用途,如 "login" / "register" / "reset_password"。 :raises windup_common.exceptions.BizException: 发送频率超限。 """ @@ -53,23 +54,20 @@ def login_by_code(self, input: LoginByCodeInput) -> LoginResult: # -- 登出 ------------------------------------------------------------ @abstractmethod - def logout(self, session_token: str) -> None: - """销毁会话。""" + def logout(self, refresh_token: str) -> None: + """销毁 refresh_token。""" - # -- OAuth ----------------------------------------------------------- - # 第三方认证暂不设计、不实现。保留该区域作为后续扩展占位。 - # 相关 authorize / callback / bind 接口和 UserOAuth 模型暂时停用。 # -- 会话管理 --------------------------------------------------------- @abstractmethod - def validate_session(self, session_token: str) -> User | None: - """校验会话并返回用户,过期 / 无效返回 ``None``。""" + def validate_access_token(self, token: str) -> UserView | None: + """校验 access_token 并返回用户,过期 / 无效返回 ``None``。""" @abstractmethod - def refresh_session(self, session_token: str) -> str: - """刷新会话,返回新 token;旧 token 立即失效。 + def refresh_tokens(self, refresh_token: str) -> LoginResult: + """刷新 token,返回新的 access+refresh。 - :raises windup_common.exceptions.BizException: 会话无效。 + :raises windup_common.exceptions.BizException: refresh token 无效 / 已撤销。 """ # -- 密码 ------------------------------------------------------------ @@ -84,11 +82,9 @@ def change_password(self, user_id: int, input: ChangePasswordInput) -> None: # -- 查询 ------------------------------------------------------------ @abstractmethod - def get_by_id(self, user_id: int) -> User | None: + def get_by_id(self, user_id: int) -> UserView | None: """按 ID 查询用户。""" @abstractmethod - def get_by_email(self, email: str) -> User | None: + def get_by_email(self, email: str) -> UserView | None: """按邮箱查询用户。""" - - # 第三方认证暂不设计、不实现,因此没有 OAuth 绑定查询接口。 \ No newline at end of file diff --git a/backend/packages/app/src/windup_app/server/user/model.py b/backend/packages/app/src/windup_app/server/user/model.py index 9e48b9a4..a8821a09 100644 --- a/backend/packages/app/src/windup_app/server/user/model.py +++ b/backend/packages/app/src/windup_app/server/user/model.py @@ -1,16 +1,73 @@ """用户领域模型。 -与 ``windup_user`` / ``windup_user_oauth`` 表一一对应, -字段名与数据库列名保持一致,方便后续 ORM 映射。 +与 ``windup_user`` 表一一对应,字段名与数据库列名保持一致。 + +ORM 模型 +-------- + +:: + + windup_user + ├── id BigInteger PK: 自增主键 + ├── email String(255) UNIQUE: 邮箱 + ├── password_hash String(255): bcrypt 哈希 + ├── nickname String(50) NULL: 昵称 + ├── email_verified_at DateTime(tz) NULL: 邮箱验证时间 + ├── status SmallInteger: 0=正常, 1=封禁 + ├── last_login_at DateTime(tz) NULL: 最后登录 + ├── create_at DateTime(tz): 创建时间 + └── update_at DateTime(tz): 更新时间 """ from dataclasses import dataclass, field from datetime import datetime, timezone from enum import IntEnum +from sqlalchemy import BigInteger, DateTime, Integer, SmallInteger, String +from sqlalchemy.orm import Mapped, mapped_column + +from windup_framework.db import Base + + +# -- ORM ---------------------------------------------------------------- + + +class User(Base): + """用户表。""" + + __tablename__ = "windup_user" + + id: Mapped[int] = mapped_column( + BigInteger().with_variant(Integer, "sqlite"), + primary_key=True, + autoincrement=True, + ) + email: Mapped[str] = mapped_column(String(255), unique=True, nullable=False) + password_hash: Mapped[str] = mapped_column(String(255), nullable=False, default="") + nickname: Mapped[str | None] = mapped_column(String(50), nullable=True) + email_verified_at: Mapped[datetime | None] = mapped_column( + DateTime(timezone=True), nullable=True + ) + status: Mapped[int] = mapped_column(SmallInteger, nullable=False, default=0) + last_login_at: Mapped[datetime | None] = mapped_column( + DateTime(timezone=True), nullable=True + ) + create_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), + nullable=False, + default=lambda: datetime.now(timezone.utc), + ) + update_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), + nullable=False, + default=lambda: datetime.now(timezone.utc), + onupdate=lambda: datetime.now(timezone.utc), + ) + # -- 枚举 ---------------------------------------------------------------- + class UserStatus(IntEnum): """用户状态。 @@ -22,22 +79,15 @@ class UserStatus(IntEnum): BANNED = 1 -class OAuthProvider(str): - """第三方登录平台(值约束)。""" - - GITHUB = "github" - GOOGLE = "google" - +# -- 领域模型 ------------------------------------------------------------ -# -- 数据模型 ------------------------------------------------------------ @dataclass -class User: - """用户(对应 ``windup_user`` 表)。""" +class UserView: + """用户视图(脱敏,不含 password_hash)。""" id: int | None = None email: str | None = None - password_hash: str = "" nickname: str | None = None email_verified_at: datetime | None = None status: UserStatus = UserStatus.NORMAL @@ -54,27 +104,16 @@ def is_email_verified(self) -> bool: return self.email_verified_at is not None -@dataclass -class UserOAuth: - """第三方登录绑定(对应 ``windup_user_oauth`` 表)。""" - - id: int | None = None - user_id: int = 0 - provider: str = "" # "github" / "google" - provider_user_id: str = "" - provider_email: str | None = None - create_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc)) - update_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc)) - - # -- 输入/输出模型 -------------------------------------------------------- + @dataclass class RegisterInput: """邮箱注册入参。""" email: str password: str + code: str nickname: str | None = None @@ -84,6 +123,7 @@ class LoginByPasswordInput: email: str password: str + code: str @dataclass @@ -94,15 +134,6 @@ class LoginByCodeInput: code: str -@dataclass -class OAuthCallbackInput: - """OAuth 回调入参。""" - - provider: str # "github" / "google" - code: str - state: str # CSRF 防护 - - @dataclass class ChangePasswordInput: """修改密码入参。""" @@ -113,11 +144,8 @@ class ChangePasswordInput: @dataclass class LoginResult: - """登录结果。 - - ``session_token`` 由调用方通过 Set-Cookie 写入客户端; - ``user`` 返回脱敏后的用户信息(不含 password_hash)。 - """ + """登录结果。""" - user: User - session_token: str \ No newline at end of file + user: UserView + access_token: str + refresh_token: str diff --git a/backend/packages/app/src/windup_app/server/user/service.py b/backend/packages/app/src/windup_app/server/user/service.py new file mode 100644 index 00000000..d1543dab --- /dev/null +++ b/backend/packages/app/src/windup_app/server/user/service.py @@ -0,0 +1,438 @@ +"""用户领域服务的 SQLAlchemy + Redis 实现。 + +:class:`SqlAlchemyUserService` 继承 :class:`UserService` 接口,用同步 +SQLAlchemy session 落库,Redis 存储验证码与 refresh_token。 + +事务边界由 ``windup_framework.db.get_session`` 依赖负责——成功 commit、异常 +rollback,故本实现只 ``flush``(把变更发到当前事务、取回生成的主键),不 commit。 +""" + +import hashlib +import logging +import random +import string +import uuid +from datetime import datetime, timezone + +import bcrypt +import jwt +import redis as redis_lib +from sqlalchemy import select +from sqlalchemy.orm import Session + +from windup_common.enums.biz_code import BizCode +from windup_common.exceptions import BizException + +from windup_app.server.user.interface import UserService +from windup_app.server.user.model import ( + ChangePasswordInput, + LoginByCodeInput, + LoginByPasswordInput, + LoginResult, + RegisterInput, + User, + UserStatus, + UserView, +) +from windup_framework.config.jwt import settings as jwt_settings +from windup_framework.providers.email import email_provider +from windup_framework.db.redis import get_redis + +logger = logging.getLogger("windup.user.service") + +# -- JWT 配置 ------------------------------------------------------------- + +JWT_SECRET = jwt_settings.secret +JWT_ALGORITHM = "HS256" +ACCESS_TOKEN_EXPIRE_SECONDS = 15 * 60 # 15 分钟 +REFRESH_TOKEN_EXPIRE_SECONDS = 7 * 24 * 3600 # 7 天 + +# -- 密码哈希 ------------------------------------------------------------- + + +def _hash_password(password: str) -> str: + """bcrypt 哈希密码。""" + return bcrypt.hashpw(password.encode(), bcrypt.gensalt()).decode() + + +def _verify_password(password: str, hashed: str) -> bool: + """验证密码。""" + return bcrypt.checkpw(password.encode(), hashed.encode()) + +# -- Redis key 前缀 ------------------------------------------------------- + +VERIFY_COOLDOWN_KEY = "verify:cooldown:{email}" +VERIFY_CODE_KEY = "verify:{purpose}:{email}" +REFRESH_TOKEN_KEY = "refresh:{token_hash}" +RATELIMIT_SENSITIVE_KEY = "ratelimit:sensitive:{ip}" + +VERIFY_CODE_TTL = 300 # 5 分钟 +COOLDOWN_TTL = 60 # 60 秒 + + +def _hash_token(token: str) -> str: + """SHA256 哈希 token,用作 Redis key。""" + return hashlib.sha256(token.encode()).hexdigest() + + +def _generate_code() -> str: + """生成 6 位数字验证码。""" + return "".join(random.choices(string.digits, k=6)) + + +# -- User → UserView 转换 ------------------------------------------------ + + +def _to_view(user: User) -> UserView: + """ORM User → 脱敏 UserView。""" + return UserView( + id=user.id, + email=user.email, + nickname=user.nickname, + email_verified_at=user.email_verified_at, + status=UserStatus(user.status), + last_login_at=user.last_login_at, + create_at=user.create_at, + update_at=user.update_at, + ) + + +# -- JWT 工具函数 --------------------------------------------------------- + + +def create_access_token(user_id: int, email: str) -> str: + """签发 access_token。""" + now = datetime.now(timezone.utc) + payload = { + "sub": str(user_id), + "email": email, + "type": "access", + "iat": now, + "exp": now.timestamp() + ACCESS_TOKEN_EXPIRE_SECONDS, + } + return jwt.encode(payload, JWT_SECRET, algorithm=JWT_ALGORITHM) + + +def create_refresh_token(user_id: int, email: str = "") -> tuple[str, str]: + """签发 refresh_token,返回 (token, jti)。""" + now = datetime.now(timezone.utc) + jti = str(uuid.uuid4()) + payload = { + "sub": str(user_id), + "email": email, + "type": "refresh", + "jti": jti, + "iat": now, + "exp": now.timestamp() + REFRESH_TOKEN_EXPIRE_SECONDS, + } + token = jwt.encode(payload, JWT_SECRET, algorithm=JWT_ALGORITHM) + return token, jti + + +def decode_token(token: str) -> dict: + """解码并验证 JWT,失败抛 BizException。""" + try: + return jwt.decode(token, JWT_SECRET, algorithms=[JWT_ALGORITHM]) + except jwt.ExpiredSignatureError: + raise BizException("token 已过期", code=BizCode.UNAUTHORIZED) from None + except jwt.InvalidTokenError: + raise BizException("token 无效", code=BizCode.UNAUTHORIZED) from None + + +# -- Service 实现 --------------------------------------------------------- + + +class SqlAlchemyUserService(UserService): + """基于 SQLAlchemy session + Redis 的用户服务实现。""" + + def __init__(self) -> None: + self._redis: redis_lib.Redis | None = None + + @property + def redis(self) -> redis_lib.Redis: + if self._redis is None: + self._redis = get_redis() + return self._redis + + # -- 注册 ------------------------------------------------------------ + + def register_by_email(self, input: RegisterInput) -> LoginResult: + # 检查邮箱是否已注册(通过全局 session,这里需要外部传入) + # 由于接口签名不含 session,改为类级持有或工厂注入 + # 但当前项目模式是 service 单例 + session 由调用方传入 + # 此处需要重构:register 不走 session 查询,直接用内部方法 + raise NotImplementedError("请通过 API 层调用带 session 的版本") + + def register_by_email_with_session( + self, session: Session, input: RegisterInput + ) -> LoginResult: + """邮箱+验证码+密码注册(带 session)。""" + # 校验验证码 + self._verify_code(input.email, input.code, "register") + + # 检查邮箱唯一 + existing = session.scalar( + select(User.id).where(User.email == input.email).limit(1) + ) + if existing is not None: + raise BizException("邮箱已注册", code=BizCode.BAD_REQUEST) + + user = User( + email=input.email, + password_hash=_hash_password(input.password), + nickname=input.nickname, + email_verified_at=datetime.now(timezone.utc), # 注册即验证(已通过验证码校验) + ) + session.add(user) + session.flush() + + # 注册即登录,签发 token + access_token = create_access_token(user.id, user.email) + refresh_token, jti = create_refresh_token(user.id, user.email) + self._store_refresh_token(jti, user.id) + + logger.info("[WINDUP] 用户注册成功 | user_id=%s email=%s", user.id, user.email) + return LoginResult( + user=_to_view(user), + access_token=access_token, + refresh_token=refresh_token, + ) + + # -- 登录 ------------------------------------------------------------ + + def login_by_password(self, input: LoginByPasswordInput) -> LoginResult: + raise NotImplementedError("请通过 API 层调用带 session 的版本") + + def login_by_password_with_session( + self, session: Session, input: LoginByPasswordInput + ) -> LoginResult: + """邮箱+密码+验证码登录(带 session)。""" + # 校验验证码 + self._verify_code(input.email, input.code, "login") + + user = session.scalar(select(User).where(User.email == input.email)) + if user is None: + raise BizException("邮箱或密码错误", code=BizCode.BAD_REQUEST) + + if not _verify_password(input.password, user.password_hash): + raise BizException("邮箱或密码错误", code=BizCode.BAD_REQUEST) + + if user.status == UserStatus.BANNED: + raise BizException("账号已被封禁", code=BizCode.BAD_REQUEST) + + # 更新最后登录时间 + user.last_login_at = datetime.now(timezone.utc) + session.flush() + + access_token = create_access_token(user.id, user.email) + refresh_token, jti = create_refresh_token(user.id, user.email) + self._store_refresh_token(jti, user.id) + + logger.info("[WINDUP] 用户登录成功 | user_id=%s email=%s", user.id, user.email) + return LoginResult( + user=_to_view(user), + access_token=access_token, + refresh_token=refresh_token, + ) + + # -- 验证码 ---------------------------------------------------------- + + def send_verification_code(self, email: str, purpose: str) -> None: + """发送邮箱验证码。""" + # 频率限制 + cooldown_key = VERIFY_COOLDOWN_KEY.format(email=email) + if self.redis.get(cooldown_key): + raise BizException("发送过于频繁,请稍后再试", code=BizCode.TOO_MANY_REQUESTS) + + code = _generate_code() + code_key = VERIFY_CODE_KEY.format(purpose=purpose, email=email) + + # 存储验证码 + 设置冷却 + pipe = self.redis.pipeline() + pipe.setex(code_key, VERIFY_CODE_TTL, code) + pipe.setex(cooldown_key, COOLDOWN_TTL, "1") + pipe.execute() + + # 发送邮件 + email_provider.send_verification_code(email, code) + logger.info("[WINDUP] 验证码已发送 | email=%s purpose=%s", email, purpose) + + def _verify_code(self, email: str, code: str, purpose: str) -> None: + """校验验证码,失败抛 BizException。""" + code_key = VERIFY_CODE_KEY.format(purpose=purpose, email=email) + stored_code = self.redis.get(code_key) + if stored_code is None: + raise BizException("验证码已过期", code=BizCode.BAD_REQUEST) + if stored_code != code: + raise BizException("验证码错误", code=BizCode.BAD_REQUEST) + # 验证通过,删除验证码 + self.redis.delete(code_key) + + def login_by_code(self, input: LoginByCodeInput) -> LoginResult: + raise NotImplementedError("请通过 API 层调用带 session 的版本") + + def login_by_code_with_session( + self, session: Session, input: LoginByCodeInput + ) -> LoginResult: + """邮箱+验证码登录,无账号自动注册(带 session)。""" + # 校验验证码 + self._verify_code(input.email, input.code, "login") + + # 查找或创建用户 + user = session.scalar(select(User).where(User.email == input.email)) + if user is None: + user = User(email=input.email, email_verified_at=datetime.now(timezone.utc)) + session.add(user) + session.flush() + logger.info("[WINDUP] 验证码自动注册 | user_id=%s email=%s", user.id, user.email) + else: + if user.status == UserStatus.BANNED: + raise BizException("账号已被封禁", code=BizCode.BAD_REQUEST) + # 标记邮箱已验证 + if user.email_verified_at is None: + user.email_verified_at = datetime.now(timezone.utc) + + user.last_login_at = datetime.now(timezone.utc) + session.flush() + + access_token = create_access_token(user.id, user.email) + refresh_token, jti = create_refresh_token(user.id, user.email) + self._store_refresh_token(jti, user.id) + + return LoginResult( + user=_to_view(user), + access_token=access_token, + refresh_token=refresh_token, + ) + + # -- 登出 ------------------------------------------------------------ + + def logout(self, refresh_token: str) -> None: + """撤销 refresh_token。""" + payload = decode_token(refresh_token) + if payload.get("type") != "refresh": + raise BizException("token 类型错误", code=BizCode.UNAUTHORIZED) + + jti = payload.get("jti") + if jti: + token_hash = _hash_token(jti) + self.redis.delete(REFRESH_TOKEN_KEY.format(token_hash=token_hash)) + + logger.info("[WINDUP] 用户登出 | user_id=%s", payload.get("sub")) + + # -- Token 验证 ------------------------------------------------------ + + def validate_access_token(self, token: str) -> UserView | None: + """校验 access_token,返回 UserView 或 None。""" + try: + payload = decode_token(token) + except BizException: + return None + + if payload.get("type") != "access": + return None + + return UserView( + id=int(payload["sub"]), + email=payload.get("email", ""), + ) + + def refresh_tokens(self, refresh_token: str) -> LoginResult: + """刷新 token。""" + payload = decode_token(refresh_token) + if payload.get("type") != "refresh": + raise BizException("token 类型错误", code=BizCode.UNAUTHORIZED) + + jti = payload.get("jti") + if not jti: + raise BizException("token 无效", code=BizCode.UNAUTHORIZED) + + token_hash = _hash_token(jti) + redis_key = REFRESH_TOKEN_KEY.format(token_hash=token_hash) + user_id_str = self.redis.get(redis_key) + + if user_id_str is None: + raise BizException("refresh token 已失效", code=BizCode.UNAUTHORIZED) + + user_id = int(user_id_str) + + # 撤销旧 token + self.redis.delete(redis_key) + + # 签发新 token(需要 email,从旧 token payload 取) + email = payload.get("email", "") + new_access = create_access_token(user_id, email) + new_refresh, new_jti = create_refresh_token(user_id, email) + self._store_refresh_token(new_jti, user_id) + + logger.info("[WINDUP] token 已刷新 | user_id=%s", user_id) + return LoginResult( + user=UserView(id=user_id, email=email), + access_token=new_access, + refresh_token=new_refresh, + ) + + # -- 密码 ------------------------------------------------------------ + + def change_password(self, user_id: int, input: ChangePasswordInput) -> None: + raise NotImplementedError("请通过 API 层调用带 session 的版本") + + def change_password_with_session( + self, session: Session, user_id: int, input: ChangePasswordInput + ) -> None: + """修改密码(带 session)。""" + user = session.get(User, user_id) + if user is None: + raise BizException("用户不存在", code=BizCode.NOT_FOUND) + + if not _verify_password(input.old_password, user.password_hash): + raise BizException("旧密码错误", code=BizCode.BAD_REQUEST) + + user.password_hash = _hash_password(input.new_password) + session.flush() + + # 修改密码后撤销该用户所有 refresh_token + self._revoke_all_user_tokens(user_id) + logger.info("[WINDUP] 密码已修改 | user_id=%s", user_id) + + # -- 查询 ------------------------------------------------------------ + + def get_by_id(self, user_id: int) -> UserView | None: + # 需要 session,由 API 层直接查 ORM + raise NotImplementedError("请通过 API 层直接查询 ORM") + + def get_by_email(self, email: str) -> UserView | None: + raise NotImplementedError("请通过 API 层直接查询 ORM") + + def get_by_id_with_session(self, session: Session, user_id: int) -> UserView | None: + user = session.get(User, user_id) + return _to_view(user) if user else None + + def get_by_email_with_session(self, session: Session, email: str) -> UserView | None: + user = session.scalar(select(User).where(User.email == email)) + return _to_view(user) if user else None + + # -- 内部方法 -------------------------------------------------------- + + def _store_refresh_token(self, jti: str, user_id: int) -> None: + """将 refresh_token 存入 Redis。""" + token_hash = _hash_token(jti) + self.redis.setex( + REFRESH_TOKEN_KEY.format(token_hash=token_hash), + REFRESH_TOKEN_EXPIRE_SECONDS, + str(user_id), + ) + + def _revoke_all_user_tokens(self, user_id: int) -> None: + """撤销指定用户的所有 refresh_token(改密时调用)。 + + 注意:Redis SCAN 在 key 数量大时有性能开销,当前阶段用户量小可接受。 + 后续可维护 user_id → token_hash 的反向索引优化。 + """ + pattern = "refresh:*" + for key in self.redis.scan_iter(match=pattern, count=100): + if self.redis.get(key) == str(user_id): + self.redis.delete(key) + + +service = SqlAlchemyUserService() diff --git a/backend/packages/app/src/windup_app/web/api/auth.py b/backend/packages/app/src/windup_app/web/api/auth.py new file mode 100644 index 00000000..610897b6 --- /dev/null +++ b/backend/packages/app/src/windup_app/web/api/auth.py @@ -0,0 +1,206 @@ +"""认证 API。 + +提供注册、登录、发码、刷新、登出、当前用户、修改密码等端点。 +""" + +import logging + +from fastapi import APIRouter, Depends, Request +from pydantic import BaseModel, ConfigDict, Field, EmailStr +from sqlalchemy.orm import Session + +from windup_common.result import Response + +from windup_framework.db import get_session + +from windup_app.server.user.model import User, UserView +from windup_app.server.user.service import service + +logger = logging.getLogger("windup.auth.api") + +router = APIRouter(prefix="/auth", tags=["auth"]) + + +# -- 请求模型 ------------------------------------------------------------ + + +class RegisterRequest(BaseModel): + """注册请求。""" + + email: EmailStr + password: str = Field(min_length=8, max_length=128) + code: str = Field(min_length=6, max_length=6, description="邮箱验证码") + nickname: str | None = Field(default=None, max_length=50) + + +class LoginRequest(BaseModel): + """密码登录请求。""" + + email: EmailStr + password: str + code: str = Field(min_length=6, max_length=6, description="邮箱验证码") + + +class SendCodeRequest(BaseModel): + """发送验证码请求。""" + + email: EmailStr + purpose: str = Field(default="login", pattern="^(login|register|reset_password)$") + + +class LoginByCodeRequest(BaseModel): + """验证码登录请求。""" + + email: EmailStr + code: str = Field(min_length=6, max_length=6) + + +class RefreshRequest(BaseModel): + """刷新 token 请求。""" + + refresh_token: str + + +class ChangePasswordRequest(BaseModel): + """修改密码请求。""" + + old_password: str + new_password: str = Field(min_length=8, max_length=128) + + +# -- 响应模型 ------------------------------------------------------------ + + +class TokenResponse(BaseModel): + """登录/注册/刷新成功响应。""" + + model_config = ConfigDict(from_attributes=True) + + access_token: str + refresh_token: str + user: UserView + + +class UserOut(BaseModel): + """用户信息响应(脱敏)。""" + + model_config = ConfigDict(from_attributes=True) + + id: int + email: str + nickname: str | None = None + email_verified_at: str | None = None + status: int = 0 + + +# -- 路由 ---------------------------------------------------------------- + + +@router.post("/register", response_model=Response[TokenResponse]) +def register(body: RegisterRequest, session: Session = Depends(get_session)): + """邮箱+验证码+密码注册,注册即登录。""" + result = service.register_by_email_with_session( + session, + type("RegisterInput", (), {"email": body.email, "password": body.password, "code": body.code, "nickname": body.nickname})(), + ) + return Response.success( + TokenResponse( + access_token=result.access_token, + refresh_token=result.refresh_token, + user=result.user, + ), + message="注册成功", + ) + + +@router.post("/login", response_model=Response[TokenResponse]) +def login(body: LoginRequest, session: Session = Depends(get_session)): + """邮箱+密码+验证码登录。""" + result = service.login_by_password_with_session( + session, + type("LoginByPasswordInput", (), {"email": body.email, "password": body.password, "code": body.code})(), + ) + return Response.success( + TokenResponse( + access_token=result.access_token, + refresh_token=result.refresh_token, + user=result.user, + ), + message="登录成功", + ) + + +@router.post("/send-code", response_model=Response[None]) +def send_code(body: SendCodeRequest): + """发送邮箱验证码。""" + service.send_verification_code(body.email, body.purpose) + return Response.success(None, message="验证码已发送") + + +@router.post("/login-by-code", response_model=Response[TokenResponse]) +def login_by_code(body: LoginByCodeRequest, session: Session = Depends(get_session)): + """验证码登录,无账号自动注册。""" + result = service.login_by_code_with_session( + session, + type("LoginByCodeInput", (), {"email": body.email, "code": body.code})(), + ) + return Response.success( + TokenResponse( + access_token=result.access_token, + refresh_token=result.refresh_token, + user=result.user, + ), + message="登录成功", + ) + + +@router.post("/refresh", response_model=Response[TokenResponse]) +def refresh(body: RefreshRequest): + """刷新 token。""" + result = service.refresh_tokens(body.refresh_token) + return Response.success( + TokenResponse( + access_token=result.access_token, + refresh_token=result.refresh_token, + user=result.user, + ), + ) + + +@router.post("/logout", response_model=Response[None]) +def logout(body: RefreshRequest): + """登出,撤销 refresh_token。""" + service.logout(body.refresh_token) + return Response.success(None, message="已登出") + + +@router.get("/me", response_model=Response[UserOut]) +def get_me(request: Request, session: Session = Depends(get_session)): + """获取当前用户信息。""" + current_user = request.state.current_user + user = session.get(User, current_user.id) + if user is None: + from windup_common.enums.biz_code import BizCode + from windup_common.exceptions import BizException + raise BizException("用户不存在", code=BizCode.NOT_FOUND) + return Response.success( + UserOut( + id=user.id, + email=user.email, + nickname=user.nickname, + email_verified_at=user.email_verified_at.isoformat() if user.email_verified_at else None, + status=user.status, + ) + ) + + +@router.post("/change-password", response_model=Response[None]) +def change_password(body: ChangePasswordRequest, request: Request, session: Session = Depends(get_session)): + """修改密码。""" + current_user = request.state.current_user + service.change_password_with_session( + session, + current_user.id, + type("ChangePasswordInput", (), {"old_password": body.old_password, "new_password": body.new_password})(), + ) + return Response.success(None, message="密码修改成功") diff --git a/backend/packages/app/src/windup_app/web/middleware/__init__.py b/backend/packages/app/src/windup_app/web/middleware/__init__.py new file mode 100644 index 00000000..49724533 --- /dev/null +++ b/backend/packages/app/src/windup_app/web/middleware/__init__.py @@ -0,0 +1 @@ +"""Web 中间件。""" diff --git a/backend/packages/app/src/windup_app/web/middleware/auth.py b/backend/packages/app/src/windup_app/web/middleware/auth.py new file mode 100644 index 00000000..4321bfa9 --- /dev/null +++ b/backend/packages/app/src/windup_app/web/middleware/auth.py @@ -0,0 +1,79 @@ +"""JWT 鉴权中间件。 + +统一拦截请求,白名单路径放行,其余路径验证 JWT access_token。 +验证通过后将用户信息注入 ``request.state.current_user``。 +""" + +from fastapi.responses import JSONResponse +from starlette.middleware.base import BaseHTTPMiddleware +from starlette.requests import Request +from starlette.responses import Response + +from windup_common.enums.biz_code import BizCode +from windup_common.result import Response as Resp + +from windup_app.server.user.service import decode_token + + +def _biz_error(msg: str, code: int) -> JSONResponse: + """BizException → JSONResponse,用于 middleware 层(绕过 ExceptionMiddleware)。""" + return JSONResponse( + status_code=200, + content=Resp.fail(msg, code=code).model_dump(mode="json"), + ) + +# -- 白名单路径(不需要鉴权)--------------------------------------------- + +AUTH_WHITELIST: set[str] = { + "/auth/register", + "/auth/login", + "/auth/send-code", + "/auth/login-by-code", + "/auth/refresh", + "/auth/logout", + "/docs", + "/openapi.json", + "/health", +} + +# 前缀白名单(如 /docs 子路径、Swagger 静态资源) +AUTH_WHITELIST_PREFIXES: tuple[str, ...] = ( + "/docs", + "/redoc", + "/openapi", +) + + +def _is_whitelisted(path: str) -> bool: + """判断路径是否在白名单中。""" + if path in AUTH_WHITELIST: + return True + return any(path.startswith(prefix) for prefix in AUTH_WHITELIST_PREFIXES) + + +class AuthMiddleware(BaseHTTPMiddleware): + """JWT 鉴权中间件。""" + + async def dispatch(self, request: Request, call_next) -> Response: + # 白名单放行 + if _is_whitelisted(request.url.path): + return await call_next(request) + + # 提取 Authorization header + auth_header = request.headers.get("authorization", "") + if not auth_header.startswith("Bearer "): + return _biz_error("未登录", BizCode.UNAUTHORIZED) + + token = auth_header[7:] # 去掉 "Bearer " 前缀 + + # 解码 + 验证 + payload = decode_token(token) + if payload.get("type") != "access": + return _biz_error("token 类型错误", BizCode.UNAUTHORIZED) + + # 注入当前用户到 request.state + request.state.current_user = type( + "CurrentUser", (), {"id": int(payload["sub"]), "email": payload.get("email", "")} + )() + + return await call_next(request) diff --git a/backend/packages/app/src/windup_app/web/middleware/ratelimit.py b/backend/packages/app/src/windup_app/web/middleware/ratelimit.py new file mode 100644 index 00000000..014b5bf2 --- /dev/null +++ b/backend/packages/app/src/windup_app/web/middleware/ratelimit.py @@ -0,0 +1,125 @@ +"""接口限流中间件。 + +基于 Redis 的滑动窗口计数器,在鉴权中间件之前执行。 +Redis 不可用时优雅降级(跳过限流)。 +""" + +import logging + +from fastapi.responses import JSONResponse +from starlette.middleware.base import BaseHTTPMiddleware +from starlette.requests import Request +from starlette.responses import Response + +from windup_common.enums.biz_code import BizCode +from windup_common.result import Response as Resp + +logger = logging.getLogger("windup.ratelimit") + +# -- 限流配置 ------------------------------------------------------------ + +# 全局 API 限流:单 IP 60 次/分钟 +GLOBAL_RATE = 60 +GLOBAL_WINDOW = 60 + +# 敏感接口限流:单 IP 10 次/分钟 +SENSITIVE_RATE = 10 +SENSITIVE_WINDOW = 60 + +# 用户级限流:120 次/分钟 +USER_RATE = 120 +USER_WINDOW = 60 + +# 敏感接口路径 +SENSITIVE_PATHS: set[str] = { + "/auth/register", + "/auth/login", + "/auth/send-code", + "/auth/login-by-code", +} + +# -- Redis key 模板 ------------------------------------------------------ + +RATELIMIT_API_KEY = "ratelimit:api:{ip}" +RATELIMIT_SENSITIVE_KEY = "ratelimit:sensitive:{ip}" +RATELIMIT_USER_KEY = "ratelimit:api:{user_id}" + + +def _get_client_ip(request: Request) -> str: + """获取客户端 IP(优先 X-Forwarded-For)。""" + forwarded = request.headers.get("x-forwarded-for") + if forwarded: + return forwarded.split(",")[0].strip() + return request.client.host if request.client else "unknown" + + +def _check_rate(redis_client, key: str, limit: int, window: int) -> bool: + """检查是否超出限流,返回 True 表示允许通过。""" + try: + current = redis_client.incr(key) + if current == 1: + redis_client.expire(key, window) + return current <= limit + except Exception: + # Redis 不可用时跳过限流 + logger.warning("[WINDUP] Redis 不可用,跳过限流检查 | key=%s", key) + return True + + +class RateLimitMiddleware(BaseHTTPMiddleware): + """接口限流中间件。""" + + def __init__(self, app) -> None: + super().__init__(app) + self._redis = None + self._redis_available = True + + @property + def redis(self): + if self._redis is None: + try: + from windup_framework.db.redis import get_redis + self._redis = get_redis() + # 测试连接 + self._redis.ping() + except Exception: + self._redis_available = False + logger.warning("[WINDUP] Redis 连接失败,限流中间件将跳过限流检查") + return None + return self._redis + + async def dispatch(self, request: Request, call_next) -> Response: + # Redis 不可用时直接放行 + if not self._redis_available or self.redis is None: + return await call_next(request) + + client_ip = _get_client_ip(request) + + # 全局限流 + if not _check_rate(self.redis, RATELIMIT_API_KEY.format(ip=client_ip), GLOBAL_RATE, GLOBAL_WINDOW): + logger.warning("[WINDUP] 全局限流触发 | ip=%s path=%s", client_ip, request.url.path) + return JSONResponse( + status_code=200, + content=Resp.fail("请求过于频繁", code=BizCode.TOO_MANY_REQUESTS).model_dump(mode="json"), + ) + + # 敏感接口额外限流 + if request.url.path in SENSITIVE_PATHS: + if not _check_rate(self.redis, RATELIMIT_SENSITIVE_KEY.format(ip=client_ip), SENSITIVE_RATE, SENSITIVE_WINDOW): + logger.warning("[WINDUP] 敏感接口限流触发 | ip=%s path=%s", client_ip, request.url.path) + return JSONResponse( + status_code=200, + content=Resp.fail("请求过于频繁,请稍后再试", code=BizCode.TOO_MANY_REQUESTS).model_dump(mode="json"), + ) + + # 用户级限流(已登录用户) + user_id = getattr(getattr(request.state, "current_user", None), "id", None) + if user_id is not None: + if not _check_rate(self.redis, RATELIMIT_USER_KEY.format(user_id=user_id), USER_RATE, USER_WINDOW): + logger.warning("[WINDUP] 用户限流触发 | user_id=%s", user_id) + return JSONResponse( + status_code=200, + content=Resp.fail("请求过于频繁", code=BizCode.TOO_MANY_REQUESTS).model_dump(mode="json"), + ) + + return await call_next(request) diff --git a/backend/packages/common/src/windup_common/enums/biz_code.py b/backend/packages/common/src/windup_common/enums/biz_code.py index f30af712..382a8596 100644 --- a/backend/packages/common/src/windup_common/enums/biz_code.py +++ b/backend/packages/common/src/windup_common/enums/biz_code.py @@ -20,6 +20,8 @@ class BizCode(int, Enum): SUCCESS = 200 # 成功 BAD_REQUEST = 400 # 请求参数校验失败 + UNAUTHORIZED = 401 # 未登录 / token 无效 NOT_FOUND = 404 # 资源不存在 + TOO_MANY_REQUESTS = 429 # 请求过于频繁 INTERNAL_ERROR = 500 # 服务器内部错误 / 兜底 MODEL_UNAVAILABLE = 503 # 模型服务不可用 diff --git a/backend/packages/framework/pyproject.toml b/backend/packages/framework/pyproject.toml index 17726b58..7084c760 100644 --- a/backend/packages/framework/pyproject.toml +++ b/backend/packages/framework/pyproject.toml @@ -11,9 +11,23 @@ dependencies = [ "psycopg[binary]>=3.2", "httpx>=0.27", "pyjwt>=2.9", - # 以下两项按选型启用: + # AI 模型适配器(providers/):chat 走 langchain,video/image 走 httpx。 + "langchain-core>=0.3", + "langchain-openai>=0.3", + # 抠图 MatteProvider:onnxruntime 直跑 u2netp(替代 rembg,其 numba 老链在 3.12 无轮子)。 + # 上限 <1.24:onnxruntime 自 1.24 起砍了 macOS Intel(x86_64)轮子;1.23.x 仍覆盖 + # Intel/arm64/Linux + py3.12,保证 Intel Mac 也能装。API 与新版一致,不改抠图代码。 + "numpy>=1.26", + "onnxruntime>=1.17,<1.24", + "pillow>=10.4", + # 对象存储(七牛 Kodo);若换 OSS/S3/MinIO 改 oss2 / boto3 / minio。 + "qiniu>=7.14", + # 用户模块:密码哈希 / Redis / 邮件 + "passlib[bcrypt]>=1.7", + "redis>=5.0", + "resend>=2.0", + # 以下按选型启用: # "rocketmq-client", # RocketMQ Python 客户端(5.x gRPC 版 / C++ 绑定版二选一) - # "minio", # 对象存储;若用 OSS/S3 换 oss2 / boto3 ] [tool.uv.sources] diff --git a/backend/packages/framework/src/windup_framework/config/email.py b/backend/packages/framework/src/windup_framework/config/email.py new file mode 100644 index 00000000..93be10bb --- /dev/null +++ b/backend/packages/framework/src/windup_framework/config/email.py @@ -0,0 +1,23 @@ +"""Resend 邮件服务配置。 + +从环境变量(或 ``.env``)读取,字段前缀 ``RESEND_``。 +""" + +from pydantic_settings import BaseSettings, SettingsConfigDict + + +class EmailSettings(BaseSettings): + """Resend 邮件服务配置。""" + + model_config = SettingsConfigDict( + env_prefix="RESEND_", + env_file=("../.env", ".env"), + env_file_encoding="utf-8", + extra="ignore", + ) + + api_key: str = "" + from_email: str = "noreply@windup.dev" + + +settings = EmailSettings() diff --git a/backend/packages/framework/src/windup_framework/config/jwt.py b/backend/packages/framework/src/windup_framework/config/jwt.py new file mode 100644 index 00000000..037cd90f --- /dev/null +++ b/backend/packages/framework/src/windup_framework/config/jwt.py @@ -0,0 +1,22 @@ +"""JWT 配置。 + +从环境变量(或 ``.env``)读取,字段前缀 ``JWT_``。 +""" + +from pydantic_settings import BaseSettings, SettingsConfigDict + + +class JWTSettings(BaseSettings): + """JWT 签名配置。""" + + model_config = SettingsConfigDict( + env_prefix="JWT_", + env_file=("../.env", ".env"), + env_file_encoding="utf-8", + extra="ignore", + ) + + secret: str = "change-me-in-production" + + +settings = JWTSettings() diff --git a/backend/packages/framework/src/windup_framework/config/redis.py b/backend/packages/framework/src/windup_framework/config/redis.py new file mode 100644 index 00000000..357994e2 --- /dev/null +++ b/backend/packages/framework/src/windup_framework/config/redis.py @@ -0,0 +1,23 @@ +"""Redis 连接配置。 + +从环境变量(或 ``.env``)读取,字段前缀 ``REDIS_``。 +本地开发默认值 ``redis://localhost:6379/0``。 +""" + +from pydantic_settings import BaseSettings, SettingsConfigDict + + +class RedisSettings(BaseSettings): + """Redis 连接配置。""" + + model_config = SettingsConfigDict( + env_prefix="REDIS_", + env_file=("../.env", ".env"), + env_file_encoding="utf-8", + extra="ignore", + ) + + url: str = "redis://localhost:6379/0" + + +settings = RedisSettings() diff --git a/backend/packages/framework/src/windup_framework/db/redis.py b/backend/packages/framework/src/windup_framework/db/redis.py new file mode 100644 index 00000000..9a22aa0e --- /dev/null +++ b/backend/packages/framework/src/windup_framework/db/redis.py @@ -0,0 +1,15 @@ +"""Redis 客户端单例。 + +模块级 import 时创建连接池,调用方通过 ``get_redis`` 获取连接。 +""" + +import redis + +from windup_framework.config.redis import settings as redis_settings + +_pool = redis.ConnectionPool.from_url(redis_settings.url, decode_responses=True) + + +def get_redis() -> redis.Redis: + """获取 Redis 连接(从连接池)。""" + return redis.Redis(connection_pool=_pool) diff --git a/backend/packages/framework/src/windup_framework/providers/email.py b/backend/packages/framework/src/windup_framework/providers/email.py new file mode 100644 index 00000000..c3ad97f6 --- /dev/null +++ b/backend/packages/framework/src/windup_framework/providers/email.py @@ -0,0 +1,51 @@ +"""邮件发送服务。 + +:class:`ResendEmailProvider` 基于 Resend SDK 实现验证码邮件发送。 +""" + +import logging +from abc import ABC, abstractmethod + +import resend + +from windup_framework.config.email import settings as email_settings + +logger = logging.getLogger("windup.email") + + +class EmailProvider(ABC): + """邮件发送抽象接口。""" + + @abstractmethod + def send_verification_code(self, to: str, code: str) -> None: + """发送验证码邮件。""" + + +class ResendEmailProvider(EmailProvider): + """基于 Resend 的邮件发送实现。""" + + def __init__(self) -> None: + resend.api_key = email_settings.api_key + + def send_verification_code(self, to: str, code: str) -> None: + """发送 6 位数字验证码邮件。""" + try: + resend.Emails.send( + { + "from": email_settings.from_email, + "to": [to], + "subject": "【Windup】您的验证码", + "html": ( + f"
您的验证码是 {code}," + f"5 分钟内有效。
" + f"如非本人操作,请忽略此邮件。
" + ), + } + ) + logger.info("[WINDUP] 验证码邮件已发送 | to=%s", to) + except Exception: + logger.exception("[WINDUP] 验证码邮件发送失败 | to=%s", to) + raise + + +email_provider = ResendEmailProvider() diff --git a/backend/tests/conftest.py b/backend/tests/conftest.py new file mode 100644 index 00000000..aa420c0a --- /dev/null +++ b/backend/tests/conftest.py @@ -0,0 +1,103 @@ +"""共享测试夹具。 + +用 SQLite 内存库(``StaticPool`` 单连接)做隔离,不依赖 Docker Postgres, +CI 友好。每个用例各自独立的 engine,互不污染。``Project`` 表按需创建在测试 +engine 上(不碰全局 Postgres engine)。 +""" + +import pytest +from fastapi.testclient import TestClient +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker +from sqlalchemy.pool import StaticPool + +from windup_app.bootstrap.app import create_app +from windup_app.server.character.model import Character +from windup_app.server.project.model import Project +from windup_app.server.user.model import User +from windup_app.server.user.service import create_access_token +from windup_framework.db import Base, get_session + + +def _make_engine(): + """单连接内存 SQLite;``check_same_thread=False`` 让 TestClient 线程可共用。""" + return create_engine( + "sqlite:///:memory:", + poolclass=StaticPool, + connect_args={"check_same_thread": False}, + ) + + +@pytest.fixture() +def engine(): + """建好 ``windup_project`` 和 ``windup_user`` 表的内存 engine。""" + engine = _make_engine() + Base.metadata.create_all(engine, tables=[Project.__table__, User.__table__, Character.__table__]) + yield engine + engine.dispose() + + +@pytest.fixture() +def db_session(engine): + """绑定到测试 engine 的 session,供 service 层单测直接传入。""" + session_local = sessionmaker(bind=engine, expire_on_commit=False) + session = session_local() + try: + yield session + finally: + session.close() + + +@pytest.fixture() +def client(engine): + """FastAPI TestClient;覆盖 ``get_session`` 指向测试 engine。 + + 不进入 lifespan 上下文(跳过 ``print_banner`` 噪音);启动逻辑无 DB 依赖。 + """ + session_local = sessionmaker(bind=engine, expire_on_commit=False) + + def override_get_session(): + session = session_local() + try: + yield session + session.commit() + except Exception: + session.rollback() + raise + finally: + session.close() + + app = create_app() + app.dependency_overrides[get_session] = override_get_session + yield TestClient(app) + app.dependency_overrides.clear() + + +@pytest.fixture() +def auth_client(engine): + """带认证 token 的 FastAPI TestClient。 + + 自动在请求头中添加 Authorization Bearer token,绕过鉴权中间件。 + """ + session_local = sessionmaker(bind=engine, expire_on_commit=False) + + def override_get_session(): + session = session_local() + try: + yield session + session.commit() + except Exception: + session.rollback() + raise + finally: + session.close() + + app = create_app() + app.dependency_overrides[get_session] = override_get_session + + # 生成测试用 token + token = create_access_token(1, "test@example.com") + client = TestClient(app, headers={"Authorization": f"Bearer {token}"}) + + yield client + app.dependency_overrides.clear() diff --git a/backend/tests/test_project_api.py b/backend/tests/test_project_api.py new file mode 100644 index 00000000..c3508dbd --- /dev/null +++ b/backend/tests/test_project_api.py @@ -0,0 +1,121 @@ +"""项目 CRUD API 集成测试。 + +通过 ``TestClient`` 打全链路:请求 -> 路由 -> service -> SQLite -> 统一响应。 +验证统一响应契约(HTTP 恒 200、code 在 body、``ListResponse`` 分页字段、 +``timestamp`` 默认省略)与 400/404 业务码路径。 +""" + + +def _payload(**overrides): + """构造合法的创建请求体(对齐 ``ProjectCreate``)。""" + base = { + "user_id": 10001, + "project_name": "像素游戏", + "character_perspective": 1, + "directional_movement": 2, + "sprite_width": 64, + "sprite_height": 64, + } + base.update(overrides) + return base + + +# -- POST /projects ---------------------------------------------------------- + + +def test_create_success(auth_client): + resp = auth_client.post("/projects", json=_payload(project_name="新建")) + + assert resp.status_code == 200 + body = resp.json() + assert body["code"] == 200 + assert body["message"] == "创建成功" + assert body["data"]["id"] is not None + assert body["data"]["project_name"] == "新建" + assert body["data"]["create_at"] + assert "timestamp" not in body + + +def test_create_duplicate_name_returns_400(auth_client): + auth_client.post("/projects", json=_payload(project_name="重名")) + resp = auth_client.post("/projects", json=_payload(project_name="重名")) + + assert resp.status_code == 200 + body = resp.json() + assert body["code"] == 400 + assert body["message"] == "项目名称已存在" + assert body["data"] is None + + +def test_create_validation_error_returns_400(auth_client): + resp = auth_client.post("/projects", json=_payload(project_name="x" * 21)) + + assert resp.status_code == 200 + assert resp.json()["code"] == 400 + + +# -- GET /projects/{id} ------------------------------------------------------ + + +def test_get_success(auth_client): + created = auth_client.post("/projects", json=_payload(project_name="详情")).json()["data"] + resp = auth_client.get(f"/projects/{created['id']}") + + assert resp.json()["code"] == 200 + assert resp.json()["data"]["project_name"] == "详情" + + +def test_get_not_found_returns_404(auth_client): + resp = auth_client.get("/projects/99999") + + body = resp.json() + assert body["code"] == 404 + assert body["message"] == "项目不存在" + assert body["data"] is None + + +# -- GET /projects ----------------------------------------------------------- + + +def test_list_empty(auth_client): + resp = auth_client.get("/projects") + + body = resp.json() + assert body["code"] == 200 + assert body["data"] == [] + assert body["total"] == 0 + assert body["page"] == 1 + assert body["page_size"] == 20 + + +def test_list_paginates_and_filters(auth_client): + for i in range(3): + auth_client.post("/projects", json=_payload(user_id=10001, project_name=f"a{i}")) + auth_client.post("/projects", json=_payload(user_id=20002, project_name="other")) + + resp = auth_client.get("/projects", params={"page": 1, "page_size": 2, "user_id": 10001}) + + body = resp.json() + assert body["total"] == 3 + assert len(body["data"]) == 2 + assert [item["project_name"] for item in body["data"]] == ["a2", "a1"] + assert all(item["user_id"] == 10001 for item in body["data"]) + + +# -- DELETE /projects/{id} --------------------------------------------------- + + +def test_delete_success(auth_client): + created = auth_client.post("/projects", json=_payload(project_name="删除")).json()["data"] + resp = auth_client.delete(f"/projects/{created['id']}") + + body = resp.json() + assert body["code"] == 200 + assert body["message"] == "删除成功" + assert auth_client.get(f"/projects/{created['id']}").json()["code"] == 404 + + +def test_delete_not_found_returns_404(auth_client): + resp = auth_client.delete("/projects/99999") + + assert resp.json()["code"] == 404 diff --git a/backend/tests/test_user_service.py b/backend/tests/test_user_service.py new file mode 100644 index 00000000..b35723de --- /dev/null +++ b/backend/tests/test_user_service.py @@ -0,0 +1,366 @@ +"""``SqlAlchemyUserService`` 单元测试。 + +用 SQLite 内存库 + mock Redis + mock 邮件服务做隔离,不依赖外部服务。 +""" + +import pytest +from unittest.mock import MagicMock, patch + +from windup_common.exceptions import BizException + +from windup_app.server.user.model import ( + ChangePasswordInput, + LoginByCodeInput, + LoginByPasswordInput, + RegisterInput, + User, + UserStatus, +) +from windup_app.server.user.service import ( + SqlAlchemyUserService, + _hash_password, + _verify_password, + create_access_token, + create_refresh_token, + decode_token, +) + + +# -- Fixtures ------------------------------------------------------------ + + +@pytest.fixture() +def mock_redis(): + """Mock Redis 客户端。""" + redis_mock = MagicMock() + redis_mock.get.return_value = None + redis_mock.setex.return_value = True + redis_mock.delete.return_value = True + redis_mock.pipeline.return_value = MagicMock( + execute=MagicMock(return_value=[True, True]) + ) + return redis_mock + + +@pytest.fixture() +def service(mock_redis): + """带 mock Redis 的 UserService 实例。""" + svc = SqlAlchemyUserService() + svc._redis = mock_redis + return svc + + +@pytest.fixture() +def mock_email(): + """Mock 邮件服务。""" + with patch("windup_app.server.user.service.email_provider") as mock: + yield mock + + +# -- 密码哈希测试 -------------------------------------------------------- + + +def test_hash_password(): + hashed = _hash_password("test123") + assert hashed != "test123" + assert _verify_password("test123", hashed) is True + + +def test_verify_password_wrong(): + hashed = _hash_password("test123") + assert _verify_password("wrong", hashed) is False + + +# -- JWT 测试 ------------------------------------------------------------ + + +def test_create_and_decode_access_token(): + token = create_access_token(1, "test@example.com") + payload = decode_token(token) + + assert payload["sub"] == "1" + assert payload["email"] == "test@example.com" + assert payload["type"] == "access" + + +def test_create_and_decode_refresh_token(): + token, jti = create_refresh_token(1, "test@example.com") + payload = decode_token(token) + + assert payload["sub"] == "1" + assert payload["email"] == "test@example.com" + assert payload["type"] == "refresh" + assert payload["jti"] == jti + + +def test_decode_expired_token(): + import jwt + from datetime import datetime, timezone + from windup_app.server.user.service import JWT_SECRET + + # 创建一个已过期的 token + payload = { + "sub": "1", + "type": "access", + "exp": datetime.now(timezone.utc).timestamp() - 100, + } + token = jwt.encode(payload, JWT_SECRET, algorithm="HS256") + + with pytest.raises(BizException, match="token 已过期"): + decode_token(token) + + +def test_decode_invalid_token(): + with pytest.raises(BizException, match="token 无效"): + decode_token("invalid-token") + + +# -- 注册测试 ------------------------------------------------------------ + + +def test_register_success(db_session, service, mock_email): + # Mock Redis 验证码 + service._redis.get.return_value = "123456" + + input_data = RegisterInput( + email="new@example.com", + password="password123", + code="123456", + ) + + result = service.register_by_email_with_session(db_session, input_data) + + assert result.user.email == "new@example.com" + assert result.access_token is not None + assert result.refresh_token is not None + assert result.user.email_verified_at is not None # 注册即验证 + + +def test_register_duplicate_email(db_session, service): + # 先注册一个用户 + service._redis.get.return_value = "123456" + input_data = RegisterInput(email="dup@example.com", password="pass123", code="123456") + service.register_by_email_with_session(db_session, input_data) + + # 尝试重复注册 + with pytest.raises(BizException, match="邮箱已注册"): + service.register_by_email_with_session(db_session, input_data) + + +def test_register_wrong_code(db_session, service): + service._redis.get.return_value = "123456" + + input_data = RegisterInput( + email="new@example.com", + password="password123", + code="999999", # 错误验证码 + ) + + with pytest.raises(BizException, match="验证码错误"): + service.register_by_email_with_session(db_session, input_data) + + +def test_register_expired_code(db_session, service): + service._redis.get.return_value = None # 验证码已过期 + + input_data = RegisterInput( + email="new@example.com", + password="password123", + code="123456", + ) + + with pytest.raises(BizException, match="验证码已过期"): + service.register_by_email_with_session(db_session, input_data) + + +# -- 登录测试 ------------------------------------------------------------ + + +def test_login_success(db_session, service, mock_email): + # 先注册 + service._redis.get.return_value = "123456" + register_input = RegisterInput(email="login@example.com", password="pass123", code="123456") + service.register_by_email_with_session(db_session, register_input) + + # 登录 + service._redis.get.return_value = "654321" + login_input = LoginByPasswordInput(email="login@example.com", password="pass123", code="654321") + result = service.login_by_password_with_session(db_session, login_input) + + assert result.user.email == "login@example.com" + assert result.access_token is not None + + +def test_login_wrong_password(db_session, service, mock_email): + # 先注册 + service._redis.get.return_value = "123456" + register_input = RegisterInput(email="login@example.com", password="pass123", code="123456") + service.register_by_email_with_session(db_session, register_input) + + # 密码错误 + service._redis.get.return_value = "654321" + login_input = LoginByPasswordInput(email="login@example.com", password="wrong", code="654321") + + with pytest.raises(BizException, match="邮箱或密码错误"): + service.login_by_password_with_session(db_session, login_input) + + +def test_login_nonexistent_user(db_session, service): + service._redis.get.return_value = "123456" + login_input = LoginByPasswordInput(email="no@example.com", password="pass123", code="123456") + + with pytest.raises(BizException, match="邮箱或密码错误"): + service.login_by_password_with_session(db_session, login_input) + + +def test_login_banned_user(db_session, service, mock_email): + # 先注册 + service._redis.get.return_value = "123456" + register_input = RegisterInput(email="banned@example.com", password="pass123", code="123456") + service.register_by_email_with_session(db_session, register_input) + + # 封禁用户 + from sqlalchemy import select + user = db_session.scalar(select(User).where(User.email == "banned@example.com")) + user.status = UserStatus.BANNED + db_session.flush() + + # 尝试登录 + service._redis.get.return_value = "654321" + login_input = LoginByPasswordInput(email="banned@example.com", password="pass123", code="654321") + + with pytest.raises(BizException, match="账号已被封禁"): + service.login_by_password_with_session(db_session, login_input) + + +# -- 验证码登录测试 ------------------------------------------------------ + + +def test_login_by_code_new_user(db_session, service, mock_email): + service._redis.get.return_value = "123456" + + input_data = LoginByCodeInput(email="code@example.com", code="123456") + result = service.login_by_code_with_session(db_session, input_data) + + assert result.user.email == "code@example.com" + assert result.user.email_verified_at is not None + + +def test_login_by_code_existing_user(db_session, service, mock_email): + # 先注册 + service._redis.get.return_value = "123456" + register_input = RegisterInput(email="exist@example.com", password="pass123", code="123456") + service.register_by_email_with_session(db_session, register_input) + + # 验证码登录 + service._redis.get.return_value = "654321" + input_data = LoginByCodeInput(email="exist@example.com", code="654321") + result = service.login_by_code_with_session(db_session, input_data) + + assert result.user.email == "exist@example.com" + + +def test_login_by_code_wrong_code(db_session, service): + service._redis.get.return_value = "123456" + + input_data = LoginByCodeInput(email="code@example.com", code="999999") + + with pytest.raises(BizException, match="验证码错误"): + service.login_by_code_with_session(db_session, input_data) + + +# -- 发送验证码测试 ------------------------------------------------------ + + +def test_send_verification_code(service, mock_email): + service._redis.get.return_value = None # 无冷却 + + service.send_verification_code("test@example.com", "login") + + mock_email.send_verification_code.assert_called_once() + service._redis.pipeline.assert_called_once() + + +def test_send_verification_code_cooldown(service, mock_email): + service._redis.get.return_value = "1" # 冷却中 + + with pytest.raises(BizException, match="发送过于频繁"): + service.send_verification_code("test@example.com", "login") + + +# -- 登出测试 ------------------------------------------------------------ + + +def test_logout(service, mock_redis): + # 先创建一个 refresh token + token, jti = create_refresh_token(1, "test@example.com") + + service.logout(token) + + mock_redis.delete.assert_called_once() + + +def test_logout_invalid_token(service): + with pytest.raises(BizException): + service.logout("invalid-token") + + +# -- 刷新 token 测试 ---------------------------------------------------- + + +def test_refresh_tokens(service, mock_redis): + # 先创建一个 refresh token + token, jti = create_refresh_token(1, "test@example.com") + + # Mock Redis 返回 user_id + mock_redis.get.return_value = "1" + + result = service.refresh_tokens(token) + + assert result.access_token is not None + assert result.refresh_token is not None + assert result.user.id == 1 + + +def test_refresh_tokens_revoked(service, mock_redis): + token, jti = create_refresh_token(1, "test@example.com") + + # Mock Redis 返回 None(已撤销) + mock_redis.get.return_value = None + + with pytest.raises(BizException, match="refresh token 已失效"): + service.refresh_tokens(token) + + +# -- 修改密码测试 -------------------------------------------------------- + + +def test_change_password(db_session, service, mock_email): + # 先注册 + service._redis.get.return_value = "123456" + register_input = RegisterInput(email="change@example.com", password="oldpass123", code="123456") + result = service.register_by_email_with_session(db_session, register_input) + + # 修改密码 + change_input = ChangePasswordInput(old_password="oldpass123", new_password="newpass123") + service.change_password_with_session(db_session, result.user.id, change_input) + + # 用新密码登录 + service._redis.get.return_value = "654321" + login_input = LoginByPasswordInput(email="change@example.com", password="newpass123", code="654321") + login_result = service.login_by_password_with_session(db_session, login_input) + + assert login_result.user.email == "change@example.com" + + +def test_change_password_wrong_old(db_session, service, mock_email): + # 先注册 + service._redis.get.return_value = "123456" + register_input = RegisterInput(email="change@example.com", password="oldpass123", code="123456") + result = service.register_by_email_with_session(db_session, register_input) + + # 旧密码错误 + change_input = ChangePasswordInput(old_password="wrong", new_password="newpass123") + + with pytest.raises(BizException, match="旧密码错误"): + service.change_password_with_session(db_session, result.user.id, change_input) diff --git a/backend/uv.lock b/backend/uv.lock index cf241f39..7942c1f9 100644 --- a/backend/uv.lock +++ b/backend/uv.lock @@ -48,6 +48,98 @@ wheels = [ { url = "https://mirrors.aliyun.com/pypi/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl", hash = "sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494" }, ] +[[package]] +name = "av" +version = "18.0.0" +source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/ae/a4/570a5a35c8638aba01e739925846c35fdd6b0756a15526766d0a4dd3b7df/av-18.0.0.tar.gz", hash = "sha256:4ef7e72c3d3a872584a1215173b16e0226811037f40dcdbf75992631098df1ba" } +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/18/4a/9e3463df030e063d757fa12f0f39be6541b45b06b5bad48c2ce361b924bf/av-18.0.0-cp311-abi3-macosx_11_0_x86_64.whl", hash = "sha256:149289d40e732a6e49c9530bc245b49d9964cfd1c8c9e06778703b7d5bba6b25" }, + { url = "https://mirrors.aliyun.com/pypi/packages/77/b3/2576a44b4f39c7462ced4c17fec04c756f7b0f3c5cb940d124173e417d6a/av-18.0.0-cp311-abi3-macosx_14_0_arm64.whl", hash = "sha256:35274c20d2ad3b4774fe632bcef2e34af79858ddf899352339cc3babbc13a484" }, + { url = "https://mirrors.aliyun.com/pypi/packages/84/74/6732f17b96dc23fd23b876b2805435855abdc8a3b397142be4e581165de8/av-18.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:4d683b7747a0ba9222b8a5f81e41db5f796e7f64473454ec4fe2548e083c2fa0" }, + { url = "https://mirrors.aliyun.com/pypi/packages/6d/b9/7708c43fed7ae28b4a1bad060b4221e3334cd827cec24f7165902a6ac1f4/av-18.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:ae56b40b6f8b067a8ad2dac664fbfbabac7f7a55b9a7bb031eb99289252bc017" }, + { url = "https://mirrors.aliyun.com/pypi/packages/5a/94/eba99691d184f6a395a242d54dc370e2fd2265e95bbc98e2963a0fdbdd6c/av-18.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:ea2e8ebbce521f21b55df9400e00d721623c9020ef158f5a188a96130be0743f" }, + { url = "https://mirrors.aliyun.com/pypi/packages/c9/cf/0d7aee07fe16aa9ffdf96043c14bed5485a52c0dea4259de87aa306ecab4/av-18.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ef96dabb3e50dac249913145dff5424b302b257fd95dcb64be3c7b7a8aef16d1" }, + { url = "https://mirrors.aliyun.com/pypi/packages/76/92/810da80b12680d4c4fe235bd1b4003289be9213ac7f114b77b8ecf0e3b3e/av-18.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:0f65518a184613e41536f29e8758c8e3d8293e46bf5bef108f04f925bbfa3f44" }, + { url = "https://mirrors.aliyun.com/pypi/packages/11/85/0f121ff43dc5a70696676c98a8f1674e2fa787614c2abaacb15fa1a9bc99/av-18.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:aaf4d354d2beaa6651e4f92e54409a578bde64f79c0beef9a30b388d06f7c629" }, + { url = "https://mirrors.aliyun.com/pypi/packages/8b/f6/2509754d4d2356abc6fc0ea3d57c12ade29bac23a1fb7fc215a53ca518fb/av-18.0.0-cp311-abi3-win_arm64.whl", hash = "sha256:adac2b3833b6cb9bd6cb52664a522b94db453615b3675b1dbb26e13fe1c80da6" }, + { url = "https://mirrors.aliyun.com/pypi/packages/e2/25/4ee23a7f1609adf9b2f140c7a8ffade64a1449d89ab431d922a809eebf19/av-18.0.0-cp314-cp314t-macosx_11_0_x86_64.whl", hash = "sha256:88dd8e35e9242662b409a6a05fd24a6775d949eb05da0ba31cab4f250eacbab5" }, + { url = "https://mirrors.aliyun.com/pypi/packages/f1/f0/b9f8363d07aa4521913e483f6a30c7c164973ef01de62769bf9b97049cd8/av-18.0.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:f8f454349c402e2c8d6fa80b54eb2a3f86c00f414d2b399f01ae6dab075c6fd8" }, + { url = "https://mirrors.aliyun.com/pypi/packages/c3/e5/69397019aed280a72a43e97a252dee4295df1a9e608848452e5300ec4dab/av-18.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:88ce194c2201c6a6d40336adee8a5ddde46ed743eacb500e3ae9368d1c6d889e" }, + { url = "https://mirrors.aliyun.com/pypi/packages/37/3a/1614d74f0d676ea6745eb59553c9ad01ca25db523cba808d522e838f4f5b/av-18.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:aa15e567a018cc94a26b0ab45da676dee70c4146ace6e92e47d30cc9689cbfbe" }, + { url = "https://mirrors.aliyun.com/pypi/packages/6b/3c/5f54710d69b0ea93634134f92b49c7a2a7fd27da5486a8a7e6251ac1cfb4/av-18.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:613153e48cefc91700746dde0ad0282d4677b194cba22cc771de14c78411cf8b" }, + { url = "https://mirrors.aliyun.com/pypi/packages/26/92/8293e6a267e0591b543abd96ae01e7e8ed228509bdb4e4644a8a8395d90f/av-18.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:30404f53ca1ea7f350ac86ff22a2c04f903014758e9b33f398c5a62de34bd84f" }, + { url = "https://mirrors.aliyun.com/pypi/packages/10/0c/38ed7601277ae57dfe857d040be4762530fd728efff45c2fb8f035fef96a/av-18.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6882a48f7aec2863c96cddee3256ff2da98f7fb6cbed83cee9d7e70a8f186a6b" }, + { url = "https://mirrors.aliyun.com/pypi/packages/c8/95/0636ca04d5d89d01c49bd366d2b660cc85d1f8117c476b2be62eb0c70855/av-18.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:55a646e9afce9fdc5de5224205a8a12c7ed1ba9803145dcc876c40bfc03a109b" }, + { url = "https://mirrors.aliyun.com/pypi/packages/01/20/1e24450ea981c44ed328691496fd2774dfa9fa3c3b00fd07f72fd5614abe/av-18.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:96f594ff506a09475e5549359352332049a25d37a08f00b4623f7f6e92e45b9c" }, +] + +[[package]] +name = "bcrypt" +version = "5.0.0" +source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/d4/36/3329e2518d70ad8e2e5817d5a4cac6bba05a47767ec416c7d020a965f408/bcrypt-5.0.0.tar.gz", hash = "sha256:f748f7c2d6fd375cc93d3fba7ef4a9e3a092421b8dbf34d8d4dc06be9492dfdd" } +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/13/85/3e65e01985fddf25b64ca67275bb5bdb4040bd1a53b66d355c6c37c8a680/bcrypt-5.0.0-cp313-cp313t-macosx_10_12_universal2.whl", hash = "sha256:f3c08197f3039bec79cee59a606d62b96b16669cff3949f21e74796b6e3cd2be" }, + { url = "https://mirrors.aliyun.com/pypi/packages/44/dc/01eb79f12b177017a726cbf78330eb0eb442fae0e7b3dfd84ea2849552f3/bcrypt-5.0.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:200af71bc25f22006f4069060c88ed36f8aa4ff7f53e67ff04d2ab3f1e79a5b2" }, + { url = "https://mirrors.aliyun.com/pypi/packages/8c/cf/e82388ad5959c40d6afd94fb4743cc077129d45b952d46bdc3180310e2df/bcrypt-5.0.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:baade0a5657654c2984468efb7d6c110db87ea63ef5a4b54732e7e337253e44f" }, + { url = "https://mirrors.aliyun.com/pypi/packages/ec/86/7134b9dae7cf0efa85671651341f6afa695857fae172615e960fb6a466fa/bcrypt-5.0.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:c58b56cdfb03202b3bcc9fd8daee8e8e9b6d7e3163aa97c631dfcfcc24d36c86" }, + { url = "https://mirrors.aliyun.com/pypi/packages/cc/82/6296688ac1b9e503d034e7d0614d56e80c5d1a08402ff856a4549cb59207/bcrypt-5.0.0-cp313-cp313t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4bfd2a34de661f34d0bda43c3e4e79df586e4716ef401fe31ea39d69d581ef23" }, + { url = "https://mirrors.aliyun.com/pypi/packages/d1/18/884a44aa47f2a3b88dd09bc05a1e40b57878ecd111d17e5bba6f09f8bb77/bcrypt-5.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:ed2e1365e31fc73f1825fa830f1c8f8917ca1b3ca6185773b349c20fd606cec2" }, + { url = "https://mirrors.aliyun.com/pypi/packages/0e/8f/371a3ab33c6982070b674f1788e05b656cfbf5685894acbfef0c65483a59/bcrypt-5.0.0-cp313-cp313t-manylinux_2_34_aarch64.whl", hash = "sha256:83e787d7a84dbbfba6f250dd7a5efd689e935f03dd83b0f919d39349e1f23f83" }, + { url = "https://mirrors.aliyun.com/pypi/packages/b1/34/7e4e6abb7a8778db6422e88b1f06eb07c47682313997ee8a8f9352e5a6f1/bcrypt-5.0.0-cp313-cp313t-manylinux_2_34_x86_64.whl", hash = "sha256:137c5156524328a24b9fac1cb5db0ba618bc97d11970b39184c1d87dc4bf1746" }, + { url = "https://mirrors.aliyun.com/pypi/packages/c0/1b/54f416be2499bd72123c70d98d36c6cd61a4e33d9b89562c22481c81bb30/bcrypt-5.0.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:38cac74101777a6a7d3b3e3cfefa57089b5ada650dce2baf0cbdd9d65db22a9e" }, + { url = "https://mirrors.aliyun.com/pypi/packages/13/62/062c24c7bcf9d2826a1a843d0d605c65a755bc98002923d01fd61270705a/bcrypt-5.0.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:d8d65b564ec849643d9f7ea05c6d9f0cd7ca23bdd4ac0c2dbef1104ab504543d" }, + { url = "https://mirrors.aliyun.com/pypi/packages/d5/c8/1fdbfc8c0f20875b6b4020f3c7dc447b8de60aa0be5faaf009d24242aec9/bcrypt-5.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:741449132f64b3524e95cd30e5cd3343006ce146088f074f31ab26b94e6c75ba" }, + { url = "https://mirrors.aliyun.com/pypi/packages/a6/c1/8b84545382d75bef226fbc6588af0f7b7d095f7cd6a670b42a86243183cd/bcrypt-5.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:212139484ab3207b1f0c00633d3be92fef3c5f0af17cad155679d03ff2ee1e41" }, + { url = "https://mirrors.aliyun.com/pypi/packages/10/a6/ffb49d4254ed085e62e3e5dd05982b4393e32fe1e49bb1130186617c29cd/bcrypt-5.0.0-cp313-cp313t-win32.whl", hash = "sha256:9d52ed507c2488eddd6a95bccee4e808d3234fa78dd370e24bac65a21212b861" }, + { url = "https://mirrors.aliyun.com/pypi/packages/48/a9/259559edc85258b6d5fc5471a62a3299a6aa37a6611a169756bf4689323c/bcrypt-5.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f6984a24db30548fd39a44360532898c33528b74aedf81c26cf29c51ee47057e" }, + { url = "https://mirrors.aliyun.com/pypi/packages/2d/df/9714173403c7e8b245acf8e4be8876aac64a209d1b392af457c79e60492e/bcrypt-5.0.0-cp313-cp313t-win_arm64.whl", hash = "sha256:9fffdb387abe6aa775af36ef16f55e318dcda4194ddbf82007a6f21da29de8f5" }, + { url = "https://mirrors.aliyun.com/pypi/packages/f8/14/c18006f91816606a4abe294ccc5d1e6f0e42304df5a33710e9e8e95416e1/bcrypt-5.0.0-cp314-cp314t-macosx_10_12_universal2.whl", hash = "sha256:4870a52610537037adb382444fefd3706d96d663ac44cbb2f37e3919dca3d7ef" }, + { url = "https://mirrors.aliyun.com/pypi/packages/67/49/dd074d831f00e589537e07a0725cf0e220d1f0d5d8e85ad5bbff251c45aa/bcrypt-5.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:48f753100931605686f74e27a7b49238122aa761a9aefe9373265b8b7aa43ea4" }, + { url = "https://mirrors.aliyun.com/pypi/packages/f5/91/50ccba088b8c474545b034a1424d05195d9fcbaaf802ab8bfe2be5a4e0d7/bcrypt-5.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f70aadb7a809305226daedf75d90379c397b094755a710d7014b8b117df1ebbf" }, + { url = "https://mirrors.aliyun.com/pypi/packages/aa/e7/d7dba133e02abcda3b52087a7eea8c0d4f64d3e593b4fffc10c31b7061f3/bcrypt-5.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:744d3c6b164caa658adcb72cb8cc9ad9b4b75c7db507ab4bc2480474a51989da" }, + { url = "https://mirrors.aliyun.com/pypi/packages/33/fc/5b145673c4b8d01018307b5c2c1fc87a6f5a436f0ad56607aee389de8ee3/bcrypt-5.0.0-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a28bc05039bdf3289d757f49d616ab3efe8cf40d8e8001ccdd621cd4f98f4fc9" }, + { url = "https://mirrors.aliyun.com/pypi/packages/27/d7/1ff22703ec6d4f90e62f1a5654b8867ef96bafb8e8102c2288333e1a6ca6/bcrypt-5.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:7f277a4b3390ab4bebe597800a90da0edae882c6196d3038a73adf446c4f969f" }, + { url = "https://mirrors.aliyun.com/pypi/packages/c8/88/815b6d558a1e4d40ece04a2f84865b0fef233513bd85fd0e40c294272d62/bcrypt-5.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:79cfa161eda8d2ddf29acad370356b47f02387153b11d46042e93a0a95127493" }, + { url = "https://mirrors.aliyun.com/pypi/packages/51/8c/e0db387c79ab4931fc89827d37608c31cc57b6edc08ccd2386139028dc0d/bcrypt-5.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:a5393eae5722bcef046a990b84dff02b954904c36a194f6cfc817d7dca6c6f0b" }, + { url = "https://mirrors.aliyun.com/pypi/packages/06/83/1570edddd150f572dbe9fc00f6203a89fc7d4226821f67328a85c330f239/bcrypt-5.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7f4c94dec1b5ab5d522750cb059bb9409ea8872d4494fd152b53cca99f1ddd8c" }, + { url = "https://mirrors.aliyun.com/pypi/packages/c9/f2/ea64e51a65e56ae7a8a4ec236c2bfbdd4b23008abd50ac33fbb2d1d15424/bcrypt-5.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0cae4cb350934dfd74c020525eeae0a5f79257e8a201c0c176f4b84fdbf2a4b4" }, + { url = "https://mirrors.aliyun.com/pypi/packages/d7/d4/1a388d21ee66876f27d1a1f41287897d0c0f1712ef97d395d708ba93004c/bcrypt-5.0.0-cp314-cp314t-win32.whl", hash = "sha256:b17366316c654e1ad0306a6858e189fc835eca39f7eb2cafd6aaca8ce0c40a2e" }, + { url = "https://mirrors.aliyun.com/pypi/packages/3f/61/3291c2243ae0229e5bca5d19f4032cecad5dfb05a2557169d3a69dc0ba91/bcrypt-5.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:92864f54fb48b4c718fc92a32825d0e42265a627f956bc0361fe869f1adc3e7d" }, + { url = "https://mirrors.aliyun.com/pypi/packages/3e/89/4b01c52ae0c1a681d4021e5dd3e45b111a8fb47254a274fa9a378d8d834b/bcrypt-5.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dd19cf5184a90c873009244586396a6a884d591a5323f0e8a5922560718d4993" }, + { url = "https://mirrors.aliyun.com/pypi/packages/84/29/6237f151fbfe295fe3e074ecc6d44228faa1e842a81f6d34a02937ee1736/bcrypt-5.0.0-cp38-abi3-macosx_10_12_universal2.whl", hash = "sha256:fc746432b951e92b58317af8e0ca746efe93e66555f1b40888865ef5bf56446b" }, + { url = "https://mirrors.aliyun.com/pypi/packages/45/b6/4c1205dde5e464ea3bd88e8742e19f899c16fa8916fb8510a851fae985b5/bcrypt-5.0.0-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c2388ca94ffee269b6038d48747f4ce8df0ffbea43f31abfa18ac72f0218effb" }, + { url = "https://mirrors.aliyun.com/pypi/packages/3b/71/427945e6ead72ccffe77894b2655b695ccf14ae1866cd977e185d606dd2f/bcrypt-5.0.0-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:560ddb6ec730386e7b3b26b8b4c88197aaed924430e7b74666a586ac997249ef" }, + { url = "https://mirrors.aliyun.com/pypi/packages/17/72/c344825e3b83c5389a369c8a8e58ffe1480b8a699f46c127c34580c4666b/bcrypt-5.0.0-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d79e5c65dcc9af213594d6f7f1fa2c98ad3fc10431e7aa53c176b441943efbdd" }, + { url = "https://mirrors.aliyun.com/pypi/packages/0b/7e/d4e47d2df1641a36d1212e5c0514f5291e1a956a7749f1e595c07a972038/bcrypt-5.0.0-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2b732e7d388fa22d48920baa267ba5d97cca38070b69c0e2d37087b381c681fd" }, + { url = "https://mirrors.aliyun.com/pypi/packages/0f/c3/0ae57a68be2039287ec28bc463b82e4b8dc23f9d12c0be331f4782e19108/bcrypt-5.0.0-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:0c8e093ea2532601a6f686edbc2c6b2ec24131ff5c52f7610dd64fa4553b5464" }, + { url = "https://mirrors.aliyun.com/pypi/packages/45/2b/77424511adb11e6a99e3a00dcc7745034bee89036ad7d7e255a7e47be7d8/bcrypt-5.0.0-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:5b1589f4839a0899c146e8892efe320c0fa096568abd9b95593efac50a87cb75" }, + { url = "https://mirrors.aliyun.com/pypi/packages/43/0a/405c753f6158e0f3f14b00b462d8bca31296f7ecfc8fc8bc7919c0c7d73a/bcrypt-5.0.0-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:89042e61b5e808b67daf24a434d89bab164d4de1746b37a8d173b6b14f3db9ff" }, + { url = "https://mirrors.aliyun.com/pypi/packages/62/83/b3efc285d4aadc1fa83db385ec64dcfa1707e890eb42f03b127d66ac1b7b/bcrypt-5.0.0-cp38-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:e3cf5b2560c7b5a142286f69bde914494b6d8f901aaa71e453078388a50881c4" }, + { url = "https://mirrors.aliyun.com/pypi/packages/95/7d/47ee337dacecde6d234890fe929936cb03ebc4c3a7460854bbd9c97780b8/bcrypt-5.0.0-cp38-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:f632fd56fc4e61564f78b46a2269153122db34988e78b6be8b32d28507b7eaeb" }, + { url = "https://mirrors.aliyun.com/pypi/packages/d6/3a/43d494dfb728f55f4e1cf8fd435d50c16a2d75493225b54c8d06122523c6/bcrypt-5.0.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:801cad5ccb6b87d1b430f183269b94c24f248dddbbc5c1f78b6ed231743e001c" }, + { url = "https://mirrors.aliyun.com/pypi/packages/55/ab/a0727a4547e383e2e22a630e0f908113db37904f58719dc48d4622139b5c/bcrypt-5.0.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3cf67a804fc66fc217e6914a5635000259fbbbb12e78a99488e4d5ba445a71eb" }, + { url = "https://mirrors.aliyun.com/pypi/packages/1b/bb/461f352fdca663524b4643d8b09e8435b4990f17fbf4fea6bc2a90aa0cc7/bcrypt-5.0.0-cp38-abi3-win32.whl", hash = "sha256:3abeb543874b2c0524ff40c57a4e14e5d3a66ff33fb423529c88f180fd756538" }, + { url = "https://mirrors.aliyun.com/pypi/packages/41/aa/4190e60921927b7056820291f56fc57d00d04757c8b316b2d3c0d1d6da2c/bcrypt-5.0.0-cp38-abi3-win_amd64.whl", hash = "sha256:35a77ec55b541e5e583eb3436ffbbf53b0ffa1fa16ca6782279daf95d146dcd9" }, + { url = "https://mirrors.aliyun.com/pypi/packages/54/12/cd77221719d0b39ac0b55dbd39358db1cd1246e0282e104366ebbfb8266a/bcrypt-5.0.0-cp38-abi3-win_arm64.whl", hash = "sha256:cde08734f12c6a4e28dc6755cd11d3bdfea608d93d958fffbe95a7026ebe4980" }, + { url = "https://mirrors.aliyun.com/pypi/packages/5d/ba/2af136406e1c3839aea9ecadc2f6be2bcd1eff255bd451dd39bcf302c47a/bcrypt-5.0.0-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:0c418ca99fd47e9c59a301744d63328f17798b5947b0f791e9af3c1c499c2d0a" }, + { url = "https://mirrors.aliyun.com/pypi/packages/ac/ee/2f4985dbad090ace5ad1f7dd8ff94477fe089b5fab2040bd784a3d5f187b/bcrypt-5.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ddb4e1500f6efdd402218ffe34d040a1196c072e07929b9820f363a1fd1f4191" }, + { url = "https://mirrors.aliyun.com/pypi/packages/e4/6e/b77ade812672d15cf50842e167eead80ac3514f3beacac8902915417f8b7/bcrypt-5.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7aeef54b60ceddb6f30ee3db090351ecf0d40ec6e2abf41430997407a46d2254" }, + { url = "https://mirrors.aliyun.com/pypi/packages/36/c4/ed00ed32f1040f7990dac7115f82273e3c03da1e1a1587a778d8cea496d8/bcrypt-5.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f0ce778135f60799d89c9693b9b398819d15f1921ba15fe719acb3178215a7db" }, + { url = "https://mirrors.aliyun.com/pypi/packages/e7/c4/fa6e16145e145e87f1fa351bbd54b429354fd72145cd3d4e0c5157cf4c70/bcrypt-5.0.0-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a71f70ee269671460b37a449f5ff26982a6f2ba493b3eabdd687b4bf35f875ac" }, + { url = "https://mirrors.aliyun.com/pypi/packages/24/b4/11f8a31d8b67cca3371e046db49baa7c0594d71eb40ac8121e2fc0888db0/bcrypt-5.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f8429e1c410b4073944f03bd778a9e066e7fad723564a52ff91841d278dfc822" }, + { url = "https://mirrors.aliyun.com/pypi/packages/ac/31/79f11865f8078e192847d2cb526e3fa27c200933c982c5b2869720fa5fce/bcrypt-5.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:edfcdcedd0d0f05850c52ba3127b1fce70b9f89e0fe5ff16517df7e81fa3cbb8" }, + { url = "https://mirrors.aliyun.com/pypi/packages/d4/8d/5e43d9584b3b3591a6f9b68f755a4da879a59712981ef5ad2a0ac1379f7a/bcrypt-5.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:611f0a17aa4a25a69362dcc299fda5c8a3d4f160e2abb3831041feb77393a14a" }, + { url = "https://mirrors.aliyun.com/pypi/packages/89/48/44590e3fc158620f680a978aafe8f87a4c4320da81ed11552f0323aa9a57/bcrypt-5.0.0-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:db99dca3b1fdc3db87d7c57eac0c82281242d1eabf19dcb8a6b10eb29a2e72d1" }, + { url = "https://mirrors.aliyun.com/pypi/packages/5f/85/e4fbfc46f14f47b0d20493669a625da5827d07e8a88ee460af6cd9768b44/bcrypt-5.0.0-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:5feebf85a9cefda32966d8171f5db7e3ba964b77fdfe31919622256f80f9cf42" }, + { url = "https://mirrors.aliyun.com/pypi/packages/25/ae/479f81d3f4594456a01ea2f05b132a519eff9ab5768a70430fa1132384b1/bcrypt-5.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:3ca8a166b1140436e058298a34d88032ab62f15aae1c598580333dc21d27ef10" }, + { url = "https://mirrors.aliyun.com/pypi/packages/df/d2/36a086dee1473b14276cd6ea7f61aef3b2648710b5d7f1c9e032c29b859f/bcrypt-5.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:61afc381250c3182d9078551e3ac3a41da14154fbff647ddf52a769f588c4172" }, + { url = "https://mirrors.aliyun.com/pypi/packages/c0/f6/688d2cd64bfd0b14d805ddb8a565e11ca1fb0fd6817175d58b10052b6d88/bcrypt-5.0.0-cp39-abi3-win32.whl", hash = "sha256:64d7ce196203e468c457c37ec22390f1a61c85c6f0b8160fd752940ccfb3a683" }, + { url = "https://mirrors.aliyun.com/pypi/packages/9f/b9/9d9a641194a730bda138b3dfe53f584d61c58cd5230e37566e83ec2ffa0d/bcrypt-5.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:64ee8434b0da054d830fa8e89e1c8bf30061d539044a39524ff7dec90481e5c2" }, + { url = "https://mirrors.aliyun.com/pypi/packages/27/44/d2ef5e87509158ad2187f4dd0852df80695bb1ee0cfe0a684727b01a69e0/bcrypt-5.0.0-cp39-abi3-win_arm64.whl", hash = "sha256:f2347d3534e76bf50bca5500989d6c1d05ed64b440408057a37673282c654927" }, +] + [[package]] name = "certifi" version = "2026.7.22" @@ -139,6 +231,18 @@ wheels = [ { url = "https://mirrors.aliyun.com/pypi/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6" }, ] +[[package]] +name = "coloredlogs" +version = "15.0.1" +source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } +dependencies = [ + { name = "humanfriendly" }, +] +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/cc/c7/eed8f27100517e8c0e6b923d5f0845d0cb99763da6fdee00478f91db7325/coloredlogs-15.0.1.tar.gz", hash = "sha256:7c991aa71a4577af2f82600d8f8f3a89f936baeaf9b50a9c197da014e5bf16b0" } +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/a7/06/3d6badcf13db419e25b07041d9c7b4a2c331d3f4e7134445ec5df57714cd/coloredlogs-15.0.1-py2.py3-none-any.whl", hash = "sha256:612ee75c546f53e92e70049c9dbfcc18c935a2b9a53b66085ce9ef6a6e5c0934" }, +] + [[package]] name = "distro" version = "1.9.0" @@ -148,6 +252,28 @@ wheels = [ { url = "https://mirrors.aliyun.com/pypi/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2" }, ] +[[package]] +name = "dnspython" +version = "2.8.0" +source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/8c/8b/57666417c0f90f08bcafa776861060426765fdb422eb10212086fb811d26/dnspython-2.8.0.tar.gz", hash = "sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f" } +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af" }, +] + +[[package]] +name = "email-validator" +version = "2.3.0" +source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } +dependencies = [ + { name = "dnspython" }, + { name = "idna" }, +] +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/f5/22/900cb125c76b7aaa450ce02fd727f452243f2e91a61af068b40adba60ea9/email_validator-2.3.0.tar.gz", hash = "sha256:9fc05c37f2f6cf439ff414f8fc46d917929974a82244c20eb10231ba60c54426" } +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/de/15/545e2b6cf2e3be84bc1ed85613edd75b8aea69807a71c26f4ca6a9258e82/email_validator-2.3.0-py3-none-any.whl", hash = "sha256:80f13f623413e6b197ae73bb10bf4eb0908faf509ad8362c5edeb0be7fd450b4" }, +] + [[package]] name = "fastapi" version = "0.139.2" @@ -164,6 +290,14 @@ wheels = [ { url = "https://mirrors.aliyun.com/pypi/packages/5f/c7/cb03251d9dfb177246a9809a76f189d21df32dbd4a845951881d11323b7f/fastapi-0.139.2-py3-none-any.whl", hash = "sha256:b9ad015a835173d59865e2f5d8296fbc2b317bf56a2ba1a5bfbdd03de2fd4b1c" }, ] +[[package]] +name = "flatbuffers" +version = "25.12.19" +source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/e8/2d/d2a548598be01649e2d46231d151a6c56d10b964d94043a335ae56ea2d92/flatbuffers-25.12.19-py2.py3-none-any.whl", hash = "sha256:7634f50c427838bb021c2d66a3d1168e9d199b0607e6329399f04846d42e20b4" }, +] + [[package]] name = "greenlet" version = "3.5.4" @@ -356,6 +490,18 @@ wheels = [ { url = "https://mirrors.aliyun.com/pypi/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad" }, ] +[[package]] +name = "humanfriendly" +version = "10.0" +source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } +dependencies = [ + { name = "pyreadline3", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/cc/3f/2c29224acb2e2df4d2046e4c73ee2662023c58ff5b113c4c1adac0886c43/humanfriendly-10.0.tar.gz", hash = "sha256:6b0b831ce8f15f7300721aa49829fc4e83921a9a301cc7f606be6686a2288ddc" } +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/f0/0f/310fb31e39e2d734ccaa2c0fb981ee41f7bd5056ce9bc29b2248bd569169/humanfriendly-10.0-py2.py3-none-any.whl", hash = "sha256:1697e1a8a8f550fd43c2865cd84542fc175a61dcb779b6fee18cf6b6ccba1477" }, +] + [[package]] name = "idna" version = "3.18" @@ -365,6 +511,19 @@ wheels = [ { url = "https://mirrors.aliyun.com/pypi/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2" }, ] +[[package]] +name = "imageio" +version = "2.37.4" +source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } +dependencies = [ + { name = "numpy" }, + { name = "pillow" }, +] +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/48/62/aa770a9307508d2a2a2c62d536a49347bffe9e55322db27838d3c93d0b07/imageio-2.37.4.tar.gz", hash = "sha256:e45cbc5e83502047fb138f7f585f7f105a136a57eea5f4b3cfc6ce1b52720bd3" } +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/3e/2d/ca050652104bab2cf55e569db2a178b1b61cb041fef28307f2db383f6d9f/imageio-2.37.4-py3-none-any.whl", hash = "sha256:1ab2e22c8debf700f24c3ac43e8f95f3b3a8110c83b93411e97b4b0b2cd1c7e6" }, +] + [[package]] name = "import-linter" version = "2.13" @@ -389,6 +548,74 @@ wheels = [ { url = "https://mirrors.aliyun.com/pypi/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12" }, ] +[[package]] +name = "jiter" +version = "0.16.0" +source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/1d/1f/10936e16d8860c70698a1aa939a46aa0224813b782bce4e000e637da0b2d/jiter-0.16.0.tar.gz", hash = "sha256:7b24c3492c5f4f84a37946ad9cf504910cf6a782d6a4e0689b6673c5894b4a1c" } +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/83/2b/52ace16ed031354f0539749a49e4bf33797d82bea5137910835fa4b09793/jiter-0.16.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:67c3bc1760f8c99d805dcab4e644027142a53b1d5d861f18780ebdbd5d40b72a" }, + { url = "https://mirrors.aliyun.com/pypi/packages/94/2e/34957c2c1b661c252ba9bcc60ae0bddc27e0f7202c6073326a13c5390eec/jiter-0.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5af7780e4a26bd7d0d989592bf9ef12ebf806b74ab709223ecca37c749872ea9" }, + { url = "https://mirrors.aliyun.com/pypi/packages/88/6c/59bd309cab4460c54cf1079f3eb7fe7af6a4c895c5c957a53378693bad2b/jiter-0.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5bf78d0e05e45cfdd66558893938d59afe3d1b1a824a202039b20e607d25a72" }, + { url = "https://mirrors.aliyun.com/pypi/packages/3b/8c/f5ef7b65f0df47afa16596969defb281ebb86e96df346d62be6fd853d620/jiter-0.16.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f4444a83f946605990c98f625cdd3d2725bfb818158760c5748c653170a20e0e" }, + { url = "https://mirrors.aliyun.com/pypi/packages/2b/0b/ace4354da061ee38844a0c27dc2c21eecd27aea119e8da324bea987522d0/jiter-0.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3a23f0e4f957e1be65752d2dfac9a5a06b1917af8dc85deb639c3b9d02e31290" }, + { url = "https://mirrors.aliyun.com/pypi/packages/55/40/c0253d3772eb9dcd8e6606ee9b2d53ec8e5b814589c47f140aa585f21eaa/jiter-0.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c22a488f7b9218e245a0025a9ba6b100e2e54700831cf4cf16833a27fba3ad01" }, + { url = "https://mirrors.aliyun.com/pypi/packages/a8/d2/4839422241aa12860ce597b20068727094ba0bc480723c74924ca5bad483/jiter-0.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46add52f4ad47a08bfb1219f3e673da972191489a33016edefdb5ea55bfa8c48" }, + { url = "https://mirrors.aliyun.com/pypi/packages/e2/59/e196888a05befdda7dbe299b722d56f2f6eec65402bc34c0a3306d595feb/jiter-0.16.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:9c8a956fd72c2cf1e730d01ea080341f13aa0a97a4a33b51abebe725b7ae9ca9" }, + { url = "https://mirrors.aliyun.com/pypi/packages/ec/74/4cd9e0fca65232136400354b630fbfcd2de634e22ccbb96567725981b548/jiter-0.16.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:561926e0573ffe4a32498420a76d64b16c513e1ab413b9d28158a8764ac701e5" }, + { url = "https://mirrors.aliyun.com/pypi/packages/d9/8c/554691e48bc711299c0a293dd8a6179e24b2d66a54dc295421fcf64569c0/jiter-0.16.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:44d019fa8cdaf89bf29c71b39e3712143fdd0ac76725c6ef954f9957a5ea8730" }, + { url = "https://mirrors.aliyun.com/pypi/packages/a4/cb/01e9d69dc2cc6759d4f91e230b34489c4fdb2518992650633f9e20bece89/jiter-0.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:0df91907609837f33341b8e6fe73b95991fdaa57caf1a0fbd343dffe826f386f" }, + { url = "https://mirrors.aliyun.com/pypi/packages/79/70/2953195f1c6ad00f49fa67e13df7e60acb3dd4f387101bc15abccddd905e/jiter-0.16.0-cp312-cp312-win32.whl", hash = "sha256:51d7b836acb0108d7c77df1742332cac2a1fa04a74d6dacec46e7091f0e91274" }, + { url = "https://mirrors.aliyun.com/pypi/packages/2d/05/2909a8b10699a4d560f8c502b6b2c5f3991b682b1922c1eedda242b225bd/jiter-0.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:1878349266f8ee36ecb1375cc5ba2f115f35fd9f0a1a4119e725e379126647f7" }, + { url = "https://mirrors.aliyun.com/pypi/packages/e9/a9/6b82bb1c8d7790d602489b967b982a909e5d092875a6c2ade96444c8dfc5/jiter-0.16.0-cp312-cp312-win_arm64.whl", hash = "sha256:2ed5738ae4af18271a51a528b8811b0cbfa4a1858de9d83359e4169855d6a331" }, + { url = "https://mirrors.aliyun.com/pypi/packages/91/c0/555fc60473d30d66894ba825e63615e3be7524fac23858356afa7a38906c/jiter-0.16.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:41977aa5654023948c2dae2a81cbf9c43343954bef1cd59a154dd15a4d84c195" }, + { url = "https://mirrors.aliyun.com/pypi/packages/d0/2b/c3eaf16f5d7c9bad66ea32f40a95bd169b29a91217fcc7f081375157e99c/jiter-0.16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d28bb3c26762358dadf3e5bf0bccd29ae987d65e6988d2e6f49829c76b003c09" }, + { url = "https://mirrors.aliyun.com/pypi/packages/96/3f/02fdfc6705cad96127d883af5c34e4867f554f29ec7705ec1a46156400a9/jiter-0.16.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0542a7189c26920778658fc8fcf2af8bae05bae9924577f71804acef37996536" }, + { url = "https://mirrors.aliyun.com/pypi/packages/b2/a6/e4bda5920d4b0d7c5dfb7174ce4a6b2e4d3e11c9162c452ef0eab4cdbdbd/jiter-0.16.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8fb8de1e23a0cb2a7f53c335049c7b72b6db41aa6227cdcc0972a1de5cb39450" }, + { url = "https://mirrors.aliyun.com/pypi/packages/b7/97/4e6b59b2c6e55cbb3e183595f81ad65dcfb21c915fee5e19e335df21bc55/jiter-0.16.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b72d0b2990ca754a9102779ac98d8597b7cb31678958562214a007f909eab78e" }, + { url = "https://mirrors.aliyun.com/pypi/packages/15/e0/97e9557686d2f94f4b93786eccb7eed28e9228ad132ea8237f44727314a7/jiter-0.16.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5f91b1c27fc22a57993d5a5cb8a627cb8ed4b10502716fac1ffbfe1d19d84e8" }, + { url = "https://mirrors.aliyun.com/pypi/packages/0f/94/db768b6938e0df35c86beeba3dfbbb025c9ee5c19e1aa271f2396e50864d/jiter-0.16.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c682bea068a90b764577bdb78a60a4c1d1606daf9cd4c893832a37c7cc9d9026" }, + { url = "https://mirrors.aliyun.com/pypi/packages/c1/d6/5a59d938244a30735fe62d9433fd325f9021ea29d89780ea4596ea93bc89/jiter-0.16.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:8d031aabecc4f1b6276adfb42e3aabb77c89d468bf616600e8d3a11328929053" }, + { url = "https://mirrors.aliyun.com/pypi/packages/67/f8/c4a857f49c9af125f6bbcac7e3eee7f7978ed89682833062e2dbf62576b1/jiter-0.16.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:eab2cd170150e70153de16896a1774e3a1dca80154c56b54d7a812c479a7165e" }, + { url = "https://mirrors.aliyun.com/pypi/packages/8b/d6/5fbc2f7d6b67b754caa61a993a2e626e815dec47ffc2f9e35f01adfebec7/jiter-0.16.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:6edb63a46e65a82c26800a868e49b2cac30dd5a4218b88d74bc2c848c8ad60bb" }, + { url = "https://mirrors.aliyun.com/pypi/packages/ed/54/284f0164b64a5fed915fea6ba7e9ba9b3d8d37c67d59cf2e3bb99d45cdfe/jiter-0.16.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:659039cc50b5addcc35fcc87ae2c1833b7c0a8e5326ef631a75e4478447bcf84" }, + { url = "https://mirrors.aliyun.com/pypi/packages/13/c5/2a467585a576594384e1d2c43e1224deaafc085f24e243529cf98beef8e1/jiter-0.16.0-cp313-cp313-win32.whl", hash = "sha256:c9c53be232c2e206ef9cdbad81a48bfa74c3d3f08bcf8124630a8a748aad993e" }, + { url = "https://mirrors.aliyun.com/pypi/packages/88/6a/de61d04b9eec69c71719968d2f716532a3bc121170c44a39e14979c6be81/jiter-0.16.0-cp313-cp313-win_amd64.whl", hash = "sha256:baad945ed47f163ad833314f8e3288c396118934f94e7bbb9e243ce4b341a4fd" }, + { url = "https://mirrors.aliyun.com/pypi/packages/19/4b/b390ed59bafb3f31d008d1218578f10327714484b334439947f7e5b11e7f/jiter-0.16.0-cp313-cp313-win_arm64.whl", hash = "sha256:3c1fd2dbe1b0af19e987f03fe66c5f5bd105a2229c1aff4ab14890b24f41d21a" }, + { url = "https://mirrors.aliyun.com/pypi/packages/a7/89/bc4f1b57d5da938fd344a466396541e586d161320d70bffd929aaafcd8f4/jiter-0.16.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:b2c61484666ad42726029af0c00ef4541f0f3b5cdc550221f56c2343208018ee" }, + { url = "https://mirrors.aliyun.com/pypi/packages/65/7a/c415453e5213001bf3b411ff65dec3d303b0e76a4a2cfea9768cd4960994/jiter-0.16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:63efadc657488f45db1c676d81e704cac2abf3fdb892def1faea61db053127e2" }, + { url = "https://mirrors.aliyun.com/pypi/packages/11/fc/1f4fb7ebf9a724c7741994f4aae18fba1e2f3133df14521a79194952c34a/jiter-0.16.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf0d73f50e7b6935677854f6e8e31d499ca7064dd24734f703e060f5b237d883" }, + { url = "https://mirrors.aliyun.com/pypi/packages/a0/8d/72cadaac05ccfa7cc3a0a2232862e6c72443ca40cf300ba8b57f9f18b69b/jiter-0.16.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bf3ea07d9bc8e7d03a9fbc051295462e6dbc295b894fd72457c3136e3e43d898" }, + { url = "https://mirrors.aliyun.com/pypi/packages/58/4a/c4b0d5f651fda90a24ffce9f8d56cde462a2e09d31ae3de3c68cef34c04e/jiter-0.16.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:26798522707abb47d767db536e4148ceac1b14446bf028ee85e579a2e043cfe5" }, + { url = "https://mirrors.aliyun.com/pypi/packages/80/58/ef77879ea9aa56b50824edc5a445e226422c7a8d211f3fd2a56bcb9493cf/jiter-0.16.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bc837c1b9631be10abfe0191537fe8009838204cec7e44827401ace390ddb567" }, + { url = "https://mirrors.aliyun.com/pypi/packages/49/2e/ffbc3f254e4d8a66da3062c624a7df4b7c2b2cf9e1fe43cf394b3e104041/jiter-0.16.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49060fd70737fad59d33ba9dcc0d83247dc9e77187de26053a19c16c9f32bd69" }, + { url = "https://mirrors.aliyun.com/pypi/packages/9a/f6/0be5dc6d64a89f80aa8fec984f94dedb2973e251edcae55841d60786d578/jiter-0.16.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:adbb8edeadd431bc4477879d5d371ece7cb1334486584e0f252656dd7ffada29" }, + { url = "https://mirrors.aliyun.com/pypi/packages/da/6e/7d31243b3b91cd261dd19e9d3557fc3251a80883d3d8049c86174e7ab7af/jiter-0.16.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:31aaee5b80f672c1dc21272bcfb9cbdcfc1ea04ff50f00ed5af500b80c44fa93" }, + { url = "https://mirrors.aliyun.com/pypi/packages/25/33/51ae371fde3c88897520f62b4d5f8b27ad7103e2bb10812ff52195609853/jiter-0.16.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:6722bcef4ffc86c835574b1b2fac6b33b9fb4a889c781e67950e891591f3c55a" }, + { url = "https://mirrors.aliyun.com/pypi/packages/a0/45/6449b3d123ea439ba79507c657288f461d55049e7bcbdc2cf8eb8210f491/jiter-0.16.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:5ab4f50ff971b611d656554ea10b75f80097392c827bc32923c6eeb6386c8b00" }, + { url = "https://mirrors.aliyun.com/pypi/packages/9b/e7/fd2fb11ae3e2649333da3aa170d04d7b3000bbdc3b270f6513382fdf4e04/jiter-0.16.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:710cc51d4ebdcd3c1f70b232c1db1ea1344a075770422bbd4bede5708335acbe" }, + { url = "https://mirrors.aliyun.com/pypi/packages/26/80/f0b147a62c315a164ed2168908286ca302310824c218d3aae52b06c0c9a9/jiter-0.16.0-cp314-cp314-win32.whl", hash = "sha256:57b37fc887a32d44798e4d8ebfa7c9683ff3da1d5bf38f08d1bb3573ccb39106" }, + { url = "https://mirrors.aliyun.com/pypi/packages/5e/e6/4758a14304b4523a6f5adb2419340086aa3593bd4327c2b25b5948a90548/jiter-0.16.0-cp314-cp314-win_amd64.whl", hash = "sha256:cbd18dd5e2df96b580487b5745adf57ef64ad89ba2d9662fc3c19386acce7db8" }, + { url = "https://mirrors.aliyun.com/pypi/packages/26/be/41fa54a2e7ea41d6c99f1dc5b1f0fd4cb474680304b5d268dd518e81da3a/jiter-0.16.0-cp314-cp314-win_arm64.whl", hash = "sha256:a32d2027a9fa67f109ff245a3252ece3ccc32cc56703e1deab6cc846a59e0585" }, + { url = "https://mirrors.aliyun.com/pypi/packages/81/6b/59127338b86d9fe4d99418f5a15118bea778103ee0fe9d9dd7e0af174e95/jiter-0.16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2577196f4474ef3fc4779a088a23b0897bbf86f9ea3679c372d45b8383b43207" }, + { url = "https://mirrors.aliyun.com/pypi/packages/2d/95/49461034d5388196d3dabf98748935f017b7785d8f3f5349f834bcc4ed0d/jiter-0.16.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:616e89e008a93c01104161c75b4988e58716b01d62307ebfe161e52a56d2a818" }, + { url = "https://mirrors.aliyun.com/pypi/packages/cd/97/a4369f2fb82cb3dda13b98622f31249b2e014b223fe64ee534413ad72294/jiter-0.16.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e2e9efbe042210df657bade597f66d6d75723e3d8f45a12ea6d8167ff8bbce3" }, + { url = "https://mirrors.aliyun.com/pypi/packages/28/51/49b6ed456261646e1906016a6760367a28aacd3c24805e4e5fe64116c1db/jiter-0.16.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f4d9e473a5ce7d27fef8b848df4dc16e283893d3f53b4a585e72c9595f3c284" }, + { url = "https://mirrors.aliyun.com/pypi/packages/33/b5/5689aff4f66c5b60be63106e591dbfcba2190df97d2c9c7cf052361ddb98/jiter-0.16.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d30a4a1c87713060c8d1cc59a7b6c8fb6b8ef0a6900368014c76c87922a2929" }, + { url = "https://mirrors.aliyun.com/pypi/packages/a2/96/3ae1b85ee0d6d6cab254fb7f8da018272b932bbf2d69b07e98aa2a96c746/jiter-0.16.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bae96332410f866e5900d809298b1ed82735932986c672495f9701daacd80620" }, + { url = "https://mirrors.aliyun.com/pypi/packages/15/32/c99d7bafd78986556c95bf60ce84c6cc98786eac56066c12d7f828bb6747/jiter-0.16.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:da3d7ec75dc83bb18bca888b5edfae0656a26849056c59e05a7728badd17e7af" }, + { url = "https://mirrors.aliyun.com/pypi/packages/0e/4b/f99a8e571287c3dec766bcc18528bbe8e8fb5365522ab5e6d64c93e87066/jiter-0.16.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ee6162b77d49a9939229df666dfa8af3e656b6701b54c4c84966d740e189264e" }, + { url = "https://mirrors.aliyun.com/pypi/packages/75/69/c78a5b3f71040e34eb5917df26fb7ae9a2174cad1ccbf277512507c53a6e/jiter-0.16.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:63ffdbdae7d4499f4cda14eadc12ddcabef0fc0c081191bdc2247489cb698077" }, + { url = "https://mirrors.aliyun.com/pypi/packages/c2/f7/095b38eda4c70d03651c403f29a5590f16d12ddc5d544aac9f9cddf72277/jiter-0.16.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a111256a7193bea0759267b10385e5870949c239ed7b6ddbaaf57573edb38734" }, + { url = "https://mirrors.aliyun.com/pypi/packages/2e/c5/6a0207d90e5f656d95af98ebd0934f382d37674416f215aeda2ff8063e51/jiter-0.16.0-cp314-cp314t-win32.whl", hash = "sha256:de5ba8763e56b793561f43bed197c9ea55776daa5e9a6b91eed68a909bc9cdbf" }, + { url = "https://mirrors.aliyun.com/pypi/packages/a5/31/c757d5f30a8980fd945ce7b98be10be9e4ff59c7c42f5fd86804c2e87db8/jiter-0.16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b8a3f9a6008048fe9def7bf465180564a6e458047d2ce499149cfbe73c3ae9db" }, + { url = "https://mirrors.aliyun.com/pypi/packages/7c/a2/d88de6d313d734a544a7901353ad5db67cb38dcfcd91713b7979dafc345d/jiter-0.16.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0fa25b09b13075c46f5bc174f2690525a925a4fc2f7c82969a2bbabff22386ce" }, + { url = "https://mirrors.aliyun.com/pypi/packages/98/ab/664fd8c4be028b2bedd3d2ff08769c4ede23d0dbc87a77c62384a0515b5d/jiter-0.16.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:f17d61a28b4b3e0e3e2ba98490c70501403b4d196f78732439160e7fd3678127" }, + { url = "https://mirrors.aliyun.com/pypi/packages/1a/07/421f1d5b65493a76e16027b848aba6a7d28073ae75944fa4289cc914d39f/jiter-0.16.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:96e38eea538c8ddf853a35727c7be0741c76c13f04148ac5c116222f50ece3b3" }, + { url = "https://mirrors.aliyun.com/pypi/packages/0a/db/bba1155f01a01c3c37a89425d571da751bbedf5c54247b831a04cb971798/jiter-0.16.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d284fb8d94d5855d60c44fefcab4bf966f1da6fada73992b01f6f0c9bc0c6702" }, + { url = "https://mirrors.aliyun.com/pypi/packages/78/f7/18a1afcd64f35314b68c1f23afcd9994d0bc13e65cc77517afff4e83986d/jiter-0.16.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64d613743df53199b1aa256a7d328340da6d7078aac7705a7db9d7a791e9cfd2" }, +] + [[package]] name = "jsonpatch" version = "1.33" @@ -430,6 +657,20 @@ wheels = [ { url = "https://mirrors.aliyun.com/pypi/packages/29/56/5ef7ba14bac95b0344da18c6e8ec108dce0baf5fc054d1117702f92af29d/langchain_core-1.5.0-py3-none-any.whl", hash = "sha256:f122efee35446632b38687119fca33711abbf3b6b555e31156762298fbe78a65" }, ] +[[package]] +name = "langchain-openai" +version = "1.4.0" +source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } +dependencies = [ + { name = "langchain-core" }, + { name = "openai" }, + { name = "tiktoken" }, +] +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/89/fc/d146705e0cf6cf8865d4e873e0551452f94d6520f43fe703594ccdf95763/langchain_openai-1.4.0.tar.gz", hash = "sha256:a3acf6be0937f3970fc9e7f0aae22929c6f117e49128bd62f4d45a64b2587d8b" } +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/f1/6c/f786dfcb6711cb06449041e72b67f7e28fc77a53e2aa16866c4f0002625a/langchain_openai-1.4.0-py3-none-any.whl", hash = "sha256:7a777731fe32a913085ec85bacd5650c3f8422048b65346b63a13b36b1b4a12f" }, +] + [[package]] name = "langchain-protocol" version = "0.0.18" @@ -547,6 +788,15 @@ wheels = [ { url = "https://mirrors.aliyun.com/pypi/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8" }, ] +[[package]] +name = "mpmath" +version = "1.3.0" +source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f" } +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c" }, +] + [[package]] name = "numpy" version = "2.5.1" @@ -598,6 +848,52 @@ wheels = [ { url = "https://mirrors.aliyun.com/pypi/packages/a1/5a/4d2b1601df3602dba7a14f3348ba9bfe94a18adb428e693df6154c293831/numpy-2.5.1-cp314-cp314t-win_arm64.whl", hash = "sha256:5a6db61f9aaa57e369905c67d852045d3c4f7126405b29d09b19dec118e9c9cb" }, ] +[[package]] +name = "onnxruntime" +version = "1.23.2" +source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } +dependencies = [ + { name = "coloredlogs" }, + { name = "flatbuffers" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "protobuf" }, + { name = "sympy" }, +] +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/1b/9e/f748cd64161213adeef83d0cb16cb8ace1e62fa501033acdd9f9341fff57/onnxruntime-1.23.2-cp312-cp312-macosx_13_0_arm64.whl", hash = "sha256:b8f029a6b98d3cf5be564d52802bb50a8489ab73409fa9db0bf583eabb7c2321" }, + { url = "https://mirrors.aliyun.com/pypi/packages/91/9d/a81aafd899b900101988ead7fb14974c8a58695338ab6a0f3d6b0100f30b/onnxruntime-1.23.2-cp312-cp312-macosx_13_0_x86_64.whl", hash = "sha256:218295a8acae83905f6f1aed8cacb8e3eb3bd7513a13fe4ba3b2664a19fc4a6b" }, + { url = "https://mirrors.aliyun.com/pypi/packages/3c/35/4e40f2fba272a6698d62be2cd21ddc3675edfc1a4b9ddefcc4648f115315/onnxruntime-1.23.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:76ff670550dc23e58ea9bc53b5149b99a44e63b34b524f7b8547469aaa0dcb8c" }, + { url = "https://mirrors.aliyun.com/pypi/packages/ef/88/9cc25d2bafe6bc0d4d3c1db3ade98196d5b355c0b273e6a5dc09c5d5d0d5/onnxruntime-1.23.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f9b4ae77f8e3c9bee50c27bc1beede83f786fe1d52e99ac85aa8d65a01e9b77" }, + { url = "https://mirrors.aliyun.com/pypi/packages/c0/b4/569d298f9fc4d286c11c45e85d9ffa9e877af12ace98af8cab52396e8f46/onnxruntime-1.23.2-cp312-cp312-win_amd64.whl", hash = "sha256:25de5214923ce941a3523739d34a520aac30f21e631de53bba9174dc9c004435" }, + { url = "https://mirrors.aliyun.com/pypi/packages/3d/41/fba0cabccecefe4a1b5fc8020c44febb334637f133acefc7ec492029dd2c/onnxruntime-1.23.2-cp313-cp313-macosx_13_0_arm64.whl", hash = "sha256:2ff531ad8496281b4297f32b83b01cdd719617e2351ffe0dba5684fb283afa1f" }, + { url = "https://mirrors.aliyun.com/pypi/packages/fe/f9/2d49ca491c6a986acce9f1d1d5fc2099108958cc1710c28e89a032c9cfe9/onnxruntime-1.23.2-cp313-cp313-macosx_13_0_x86_64.whl", hash = "sha256:162f4ca894ec3de1a6fd53589e511e06ecdc3ff646849b62a9da7489dee9ce95" }, + { url = "https://mirrors.aliyun.com/pypi/packages/1c/a1/428ee29c6eaf09a6f6be56f836213f104618fb35ac6cc586ff0f477263eb/onnxruntime-1.23.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:45d127d6e1e9b99d1ebeae9bcd8f98617a812f53f46699eafeb976275744826b" }, + { url = "https://mirrors.aliyun.com/pypi/packages/f2/2b/b57c8a2466a3126dbe0a792f56ad7290949b02f47b86216cd47d857e4b77/onnxruntime-1.23.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8bace4e0d46480fbeeb7bbe1ffe1f080e6663a42d1086ff95c1551f2d39e7872" }, + { url = "https://mirrors.aliyun.com/pypi/packages/4a/93/aba75358133b3a941d736816dd392f687e7eab77215a6e429879080b76b6/onnxruntime-1.23.2-cp313-cp313-win_amd64.whl", hash = "sha256:1f9cc0a55349c584f083c1c076e611a7c35d5b867d5d6e6d6c823bf821978088" }, + { url = "https://mirrors.aliyun.com/pypi/packages/7c/3d/6830fa61c69ca8e905f237001dbfc01689a4e4ab06147020a4518318881f/onnxruntime-1.23.2-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9d2385e774f46ac38f02b3a91a91e30263d41b2f1f4f26ae34805b2a9ddef466" }, + { url = "https://mirrors.aliyun.com/pypi/packages/b6/ca/862b1e7a639460f0ca25fd5b6135fb42cf9deea86d398a92e44dfda2279d/onnxruntime-1.23.2-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e2b9233c4947907fd1818d0e581c049c41ccc39b2856cc942ff6d26317cee145" }, +] + +[[package]] +name = "openai" +version = "2.50.0" +source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "httpx" }, + { name = "jiter" }, + { name = "pydantic" }, + { name = "sniffio" }, + { name = "tqdm" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/d8/f5/e7735f2af272ee179a287911a698b3cbdb59d7a4ac4874571363adf1e4de/openai-2.50.0.tar.gz", hash = "sha256:5128f7caf4a6b01aefd6e7e93efe170a2c3427b8de286b9af5cdff3aa47e02c8" } +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/00/ca/db315b3bb748c26c644a3f85b7d509e774354d6518d47080b1446005ee41/openai-2.50.0-py3-none-any.whl", hash = "sha256:90bdddcc5a2fa529b350fac9c5780d87e5c361dcc6090ab57b0d470b0d7af7fa" }, +] + [[package]] name = "orjson" version = "3.11.9" @@ -699,6 +995,20 @@ wheels = [ { url = "https://mirrors.aliyun.com/pypi/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e" }, ] +[[package]] +name = "passlib" +version = "1.7.4" +source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/b6/06/9da9ee59a67fae7761aab3ccc84fa4f3f33f125b370f1ccdb915bf967c11/passlib-1.7.4.tar.gz", hash = "sha256:defd50f72b65c5402ab2c573830a6978e5f202ad0d984793c8dde2c4152ebe04" } +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/3b/a4/ab6b7589382ca3df236e03faa71deac88cae040af60c071a78d254a62172/passlib-1.7.4-py2.py3-none-any.whl", hash = "sha256:aa6bca462b8d8bda89c70b382f0c298a20b5560af6cbfa2dce410c0a2fb669f1" }, +] + +[package.optional-dependencies] +bcrypt = [ + { name = "bcrypt" }, +] + [[package]] name = "pillow" version = "12.3.0" @@ -779,6 +1089,21 @@ wheels = [ { url = "https://mirrors.aliyun.com/pypi/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746" }, ] +[[package]] +name = "protobuf" +version = "7.35.1" +source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/da/01/9ef0afd7999eb9badb3a768b4aedd78c86d4c65cfaf1958ab276199e76b4/protobuf-7.35.1.tar.gz", hash = "sha256:ce115a26fe0c39a2c29973d914d327e516a6455464489fe3cd1e51a1b354f81a" } +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/10/03/8aeeb7458d22546bf64b5250ca1daeb5ff757d900e8e4a7476c6f0db843e/protobuf-7.35.1-cp310-abi3-macosx_10_9_universal2.whl", hash = "sha256:24f857477359a85c0c235261b8ba905fd51b2562f4a64ca1df5473f29850cbf6" }, + { url = "https://mirrors.aliyun.com/pypi/packages/37/4b/dfb89eb0e652a1ff073c39a59fb5e3a83cfe9b57a2c83fa6d78270101767/protobuf-7.35.1-cp310-abi3-manylinux2014_aarch64.whl", hash = "sha256:11d6b0ec246892d85215b0a13ca6e0233cf5284b68f0ac02646427f4ff88a799" }, + { url = "https://mirrors.aliyun.com/pypi/packages/0f/58/dc12f2cd484951524af6e3382c785869b9b3fb5e52ee95ae23add53ee8f9/protobuf-7.35.1-cp310-abi3-manylinux2014_s390x.whl", hash = "sha256:b73f9489a4b8b1c9cb1f8ed951c736392592edb24b9d6819f36d2e10b171d5b4" }, + { url = "https://mirrors.aliyun.com/pypi/packages/e4/be/5b3cfe508bfab6761414ff944e3366eb13be4fd71efcd69450f89ba39f43/protobuf-7.35.1-cp310-abi3-manylinux2014_x86_64.whl", hash = "sha256:74758715c53d7158fb76caf4f0cfdacc5329a4b1bb994f865d6cf302d413a1c4" }, + { url = "https://mirrors.aliyun.com/pypi/packages/d8/bc/6d6c7ba8709c85f8f2c390b2b118d6fb08a783676a572271851bf45a7d22/protobuf-7.35.1-cp310-abi3-win32.whl", hash = "sha256:353652e4efd0bca5b5fc2656abf8307ef351f0cf938c9eba09f0e09c20a25c30" }, + { url = "https://mirrors.aliyun.com/pypi/packages/0a/19/8d0cb6f20a1ef7b18f1c8986ad5783f22f84cce39c6ce9a6e645ea55192e/protobuf-7.35.1-cp310-abi3-win_amd64.whl", hash = "sha256:230a75ddfc2de4806e56696ce9640c1cdfdb6543b7cfce98d42a4c0a0e7bdb87" }, + { url = "https://mirrors.aliyun.com/pypi/packages/19/c7/5f7c636ec43e0c545e28d1f1db71990108306f7bdcb89f069ba97e428e7f/protobuf-7.35.1-py3-none-any.whl", hash = "sha256:4bc97768d8fe4ad6743c8a19403e314511ed9f6d13205b687e52421c023ac1b9" }, +] + [[package]] name = "psycopg" version = "3.3.4" @@ -852,6 +1177,11 @@ wheels = [ { url = "https://mirrors.aliyun.com/pypi/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba" }, ] +[package.optional-dependencies] +email = [ + { name = "email-validator" }, +] + [[package]] name = "pydantic-core" version = "2.46.4" @@ -959,6 +1289,15 @@ wheels = [ { url = "https://mirrors.aliyun.com/pypi/packages/a3/5e/ecf12fdb62546d64385c158514e9b2b671f7832108ef2ecd2020ce0af2d1/pyjwt-2.13.0-py3-none-any.whl", hash = "sha256:66adcc2aff09b3f1bbd95fc1e1577df8ac8723c978552fd43304c8a290ac5728" }, ] +[[package]] +name = "pyreadline3" +version = "3.5.6" +source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/b6/6d/f94028646d7bbe6d9d873c47ee7c246f2d29129d253f0d96cb6fcab70733/pyreadline3-3.5.6.tar.gz", hash = "sha256:61e53218b99656091ddb077df9e71f25850e72e030b6183b39c9b7e6e4f4a9bf" } +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/f7/5e/35c856e186b74678c24927847ad9895a51f1bc02a0c6126477a6c6040064/pyreadline3-3.5.6-py3-none-any.whl", hash = "sha256:8449b734232e42a5dcd74048e39b60db2839a4c38cf3ae2bf7707d58b5389c0d" }, +] + [[package]] name = "pytest" version = "9.1.1" @@ -1039,6 +1378,115 @@ wheels = [ { url = "https://mirrors.aliyun.com/pypi/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b" }, ] +[[package]] +name = "qiniu" +version = "7.18.0" +source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } +dependencies = [ + { name = "requests" }, +] +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/32/e5/82e5078de1204b641d6b24fdd15b3c5a87a7dd71f514e4a3cfb845a2d988/qiniu-7.18.0.tar.gz", hash = "sha256:d9edca3a1c5217c13638a08d9095cd1661f5ba6cf92ea3827949ff3d332ea4fa" } +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/1d/56/9368cd96d2132017f5748a812cb20a87263498de314e1823472cb4bdbad9/qiniu-7.18.0-py3-none-any.whl", hash = "sha256:0f1be608ac6800ad5f32690d1aa02353b6b2ff5edc78f32a2318859d375a27df" }, +] + +[[package]] +name = "redis" +version = "8.1.0" +source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/a8/99/604f0b666d4c616d891cf77ebb9db6bb21601344c051aebf1b72b9ff915f/redis-8.1.0.tar.gz", hash = "sha256:6e1a19beef9225c83efd689c7e6b7da2d5215b1f42cd13b7fc3714d0a09c7b25" } +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/66/9d/c5731f6e3608663d4d3656fd8d3aecee8b509c3082818f5a13eae925baea/redis-8.1.0-py3-none-any.whl", hash = "sha256:a4fe1aac3d3b3cc791d4b3d5931c5a956045dc951ee74d1c913ee3ac4d2ee9fb" }, +] + +[[package]] +name = "regex" +version = "2026.7.19" +source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/20/98/04b13f1ddfb63158025291c02e03eb42fbb7acb51d091d541050eb4e35e8/regex-2026.7.19.tar.gz", hash = "sha256:7e77b324909c1617cbb4c668677e2c6ae13f44d7c1de0d4f15f2e3c10f3315b5" } +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/3b/b9/d11d7e501ac8fd7d617684423ebb9561e0b998481c1e4cbc0cb212c5d74a/regex-2026.7.19-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:2cc3460cedf7579948486eab03bc9ad7089df4d7281c0f47f4afe03e8d13f02d" }, + { url = "https://mirrors.aliyun.com/pypi/packages/3f/a9/a5ab6f312f24318019170dc485d5421fe4f89e43a98640da50d95a8a7041/regex-2026.7.19-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0e9554c8785eac5cffe6300f69a91f58ba72bc88a5f8d661235ad7c6aa5b8ccd" }, + { url = "https://mirrors.aliyun.com/pypi/packages/b3/63/4cab4d7f2d384a144d420b763d97674cb70619c878ea6fcd7640d0e62143/regex-2026.7.19-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d7da47a0f248977f08e2cb659ff3c17ddc13a4d39b3a7baa0a81bf5b415430f6" }, + { url = "https://mirrors.aliyun.com/pypi/packages/22/85/102a81b218298957d4ea7d2f084fae537a71add9d6ff93c8e67284c5f45e/regex-2026.7.19-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:93db40c8de0815baab96a06e08a984bac71f989d13bab789e382158c5d426797" }, + { url = "https://mirrors.aliyun.com/pypi/packages/78/b5/dc136af5629938a037cd2b304c12240e132ec92f38be8ff9cc89af2a1f2d/regex-2026.7.19-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:66bd62c59a5427746e8c44becae1d9b99d22fb13f30f492083dfb9ad7c45cc18" }, + { url = "https://mirrors.aliyun.com/pypi/packages/e0/75/67402ae3cd9c8c988a4c805d15ee3eef015e7ca4cb112cf3e640fc1f4153/regex-2026.7.19-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1649eb39fcc9ea80c4d2f110fde2b8ab2aef3877b98f02ab9b14e961f418c511" }, + { url = "https://mirrors.aliyun.com/pypi/packages/2a/8e/096d00c7c480ef2ff4265349b14e2261d4ab787ba1f74e2e80d1c58079c3/regex-2026.7.19-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9dce8ec9695f531a1b8a6f314fd4b393adcccf2ea861db480cdf97a301d01a68" }, + { url = "https://mirrors.aliyun.com/pypi/packages/f0/41/e7ecac6edb5722417f85cc67eaf386322fbe8acf6918ec2fdc37c20dd9d0/regex-2026.7.19-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3080a7fd38ef049bd489e01c970c97dd84ff446a885b0f1f6b26d9b1ad13ce11" }, + { url = "https://mirrors.aliyun.com/pypi/packages/6f/69/03c9b3f058d66403e0ca2c938696e81d51cd4c6d47ec5265f02f96948d9a/regex-2026.7.19-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1d793a7988e04fcb1e2e135567443d82173225d657419ec09414a9b5a145b986" }, + { url = "https://mirrors.aliyun.com/pypi/packages/f6/f7/b38ab3d43f284afbb618fcd15d0e77eb786ae461ce1f6bc7494619ddc0f2/regex-2026.7.19-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:e8b0abe7d870f53ca5143895fef7d1041a0c831a140d3dc2c760dd7ba25d4a8b" }, + { url = "https://mirrors.aliyun.com/pypi/packages/15/5c/ff60ef0571121714f3cf9920bc183071e384a10b556d042e0fdb06cc07a5/regex-2026.7.19-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:4e5413bd5f13d3a4e3539ca98f70f75e7fca92518dd7f117f030ebedd10b60cb" }, + { url = "https://mirrors.aliyun.com/pypi/packages/aa/0f/bd34021162c0ab47f9a315bd56cd5642e920c8e5668a75ef6c6a6fca590d/regex-2026.7.19-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:73b133a9e6fb512858e7f065e96f1180aa46646bc74a83aea62f1d314f3dd035" }, + { url = "https://mirrors.aliyun.com/pypi/packages/2a/20/a2ca43edade0595cccfdc98636739f536d9e26898e7dbddc2b9e98898953/regex-2026.7.19-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dbe6493fbd27321b1d1f2dd4f5c7e5bd4d8b1d7cab7f32fd67db3d0b2ed8248a" }, + { url = "https://mirrors.aliyun.com/pypi/packages/5d/47/e02db4015d424fc83c00ea0ac8c5e5ec14397943de9abf909d5ce3a25931/regex-2026.7.19-cp312-cp312-win32.whl", hash = "sha256:ddd67571c10869f65a5d7dde536d1e066e306cc90de57d7de4d5f34802428bb5" }, + { url = "https://mirrors.aliyun.com/pypi/packages/08/8e/c780c131f79b42ed22d1bd7da4096c2c35f813e835acd02ef0f018bd892c/regex-2026.7.19-cp312-cp312-win_amd64.whl", hash = "sha256:e30d40268a28d54ce0437031750497004c22602b8e3ab891f759b795a003b312" }, + { url = "https://mirrors.aliyun.com/pypi/packages/3e/4c/e4d7e086449bdf379d89774bf1f89dc4a41943f3c5a6125a03905b34b5fb/regex-2026.7.19-cp312-cp312-win_arm64.whl", hash = "sha256:de9208bb427130c82a5dbfd104f92c8876fc9559278c880b3002755bbbe9c83d" }, + { url = "https://mirrors.aliyun.com/pypi/packages/5d/3d/84165e4299ff76f3a40fe1f2abf939e976f693383a08d2beea6af62bd2c1/regex-2026.7.19-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f035d9dc1d25eff9d361456572231c7d27b5ccd473ca7dc0adfce732bd006d40" }, + { url = "https://mirrors.aliyun.com/pypi/packages/02/a2/a65293e6e4cf28eb7ee1be5335a5386c40d6742e9f47fafc8fec785e16c7/regex-2026.7.19-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c42572142ed0b9d5d261ba727157c426510da78e20828b66bbb855098b8a4e38" }, + { url = "https://mirrors.aliyun.com/pypi/packages/95/47/2d0564e93d87bc48618360ddca232a2ca612bbdf53ce8465d45ca5ce14ee/regex-2026.7.19-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:40b34dd88658e4fedd2fddbf0275ac970d00614b731357f425722a3ed1983d11" }, + { url = "https://mirrors.aliyun.com/pypi/packages/07/cd/42dfbabff3dfc9603c501c0e2e2c5adbb09d127b267bf5348de0af338c15/regex-2026.7.19-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c41c63992bf1874cebb6e7f56fd7d3c007924659a604ae3d90e427d40d4fd13" }, + { url = "https://mirrors.aliyun.com/pypi/packages/df/5d/f6a4839f2b934e3eed5973fd07f5929ee97d4c98939fb275ea23c274ee16/regex-2026.7.19-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1d3372064506b94dd2c67c845f2db8062e9e9ba84d04e33cb96d7d33c11fe1ae" }, + { url = "https://mirrors.aliyun.com/pypi/packages/14/b0/b47d6c36049bc59806a50bd4c86ced70bbe058d787f80281b1d7a9b0e024/regex-2026.7.19-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fce7760bf283405b2c7999cab3da4e72f7deca6396013115e3f7a955db9760da" }, + { url = "https://mirrors.aliyun.com/pypi/packages/2a/be/ff61f28f9273658cfe23acbbac5217221f6519960ed401e61dfdab12bc35/regex-2026.7.19-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c0d702548d89d572b2929879bc883bb7a4c4709efafe4512cadee56c55c9bd15" }, + { url = "https://mirrors.aliyun.com/pypi/packages/c3/bb/8b4f7f26b333f9f79e1b453613c39bb4776f51d38ae66dd0ba31d6b354ca/regex-2026.7.19-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d446c6ac40bb6e05025ccee55b84d80fe9bf8e93010ffc4bb9484f13d498835f" }, + { url = "https://mirrors.aliyun.com/pypi/packages/09/13/610110fc5921d380516d03c26b652555f08aa0d23ea78a771231873c3638/regex-2026.7.19-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4c3501bfa814ab07b5580741f9bf78dfdfe146a04057f82df9e2402d2a975939" }, + { url = "https://mirrors.aliyun.com/pypi/packages/ca/f5/1ef9e2a83a5947c57ebff0b377cb5727c3d5ec1992317a320d035cd0dbb6/regex-2026.7.19-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c4585c3e64b4f9e583b4d2683f18f5d5d872b3d71dcf24594b74ecc23602fa96" }, + { url = "https://mirrors.aliyun.com/pypi/packages/a0/02/073af33a3ec149241d11c80acea91e722aa0adbf05addd50f251c4fe89c3/regex-2026.7.19-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:571fde9741eb0ccde23dd4e0c1d50fbae910e901fa7e629faf39b2dda740d220" }, + { url = "https://mirrors.aliyun.com/pypi/packages/81/a9/d1e9f819dc394a568ef370cd56cf25394e957a2235f8370f23b576e5a475/regex-2026.7.19-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:15b364b9b98d6d2fe1a85034c23a3180ff913f46caddc3895f6fd65186255ccc" }, + { url = "https://mirrors.aliyun.com/pypi/packages/03/3a/8ae83eda7579feacdf984e71fb9e70635fb6f832eeddca58427ec4fca926/regex-2026.7.19-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ffd8893ccc1c2fce6e0d6ca402d716fe1b29db70c7132609a05955e31b2aa8f2" }, + { url = "https://mirrors.aliyun.com/pypi/packages/4b/23/c195cbfe5a75fdec64d8f6554fd15237b837919d2c61bdc141d7c807b08b/regex-2026.7.19-cp313-cp313-win32.whl", hash = "sha256:f0fa4fa9c3632d708742baf2282f2055c11d888a790362670a403cbf48a2c404" }, + { url = "https://mirrors.aliyun.com/pypi/packages/b2/80/a11de8404b7272b70acb45c1c05987cce60b45d5693da2e176f0e390d564/regex-2026.7.19-cp313-cp313-win_amd64.whl", hash = "sha256:d51ffd3427640fa2da6ade574ceba932f210ad095f65fcc450a2b0a0d454868e" }, + { url = "https://mirrors.aliyun.com/pypi/packages/d1/29/0f5c8eff1b4f1f3d83276d365fccecf666afcc7d947420943bf394d07adb/regex-2026.7.19-cp313-cp313-win_arm64.whl", hash = "sha256:c670fe7be5b6020b76bc6e8d2196074657e1327595bca93a389e1a76ab130ad8" }, + { url = "https://mirrors.aliyun.com/pypi/packages/dc/4c/44b74742052cedda40f9ae469532a037112f7311a36669a891fba8984bb0/regex-2026.7.19-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:db47b561c9afd884baa1f96f797c9ca369872c4b65912bc691cfa99e68340af2" }, + { url = "https://mirrors.aliyun.com/pypi/packages/f0/45/bbd038b5e39ee5613a5a689290145b40058cc152c41de9cc23639d2b9734/regex-2026.7.19-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:65dcd28d3eba2ab7c2fd906485cc301392b47cc2234790d27d4e4814e02cdfda" }, + { url = "https://mirrors.aliyun.com/pypi/packages/65/38/c5bde94b4cedfd5850d64c3f08222d8e1600e84f6ee71d9b44b4b8163f74/regex-2026.7.19-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f2e7f8e2ab6c2922be02c7ec45185aa5bd771e2e57b95455ee343a44d8130dff" }, + { url = "https://mirrors.aliyun.com/pypi/packages/d7/6a/2f5e107cb26c960b781967178899daf2787a7ab151844ed3c01d6fc95474/regex-2026.7.19-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fe31f28c94402043161876a258a9c6f757cb485905c7614ce8d6cd40e6b7bdc1" }, + { url = "https://mirrors.aliyun.com/pypi/packages/37/d4/a2f963406d7d73a62eed84ba05a258afb6cad1b21aa4517443ce40506b78/regex-2026.7.19-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f8f6fa298bb4f7f58a33334406218ba74716e68feddf5e4e54cd5d8082705abf" }, + { url = "https://mirrors.aliyun.com/pypi/packages/45/a3/44be546340bedb15f13063f5e7fe16793ea4d9ea2e805d09bd174ac27724/regex-2026.7.19-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:cc1b2440423a851fad781309dd87843868f4f66a6bcd1ddb9225cf4ec2c84732" }, + { url = "https://mirrors.aliyun.com/pypi/packages/f8/f6/e0870b0fd2a40dba0074e4b76e514b21313d37946c9248453e34ec43923e/regex-2026.7.19-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8ac59a0900474a52b7c04af8196affc22bd9842acb0950df12f7b813e983609a" }, + { url = "https://mirrors.aliyun.com/pypi/packages/ae/27/957e8e22690ad6634572b39b71f130a6105f4d0718bb16849eac00fff147/regex-2026.7.19-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4896db1f4ce0576765b8272aa922df324e0f5b9bb2c3d03044ff32a7234a9aba" }, + { url = "https://mirrors.aliyun.com/pypi/packages/76/a4/186e410941e731037c01166069ab86da9f65e8f8110c18009ccf4bd623ee/regex-2026.7.19-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4e6883a021db30511d9fb8cfb0f222ce1f2c369f7d4d8b0448f449a93ba0bdfc" }, + { url = "https://mirrors.aliyun.com/pypi/packages/73/9f/e4e10e023d291d64a33e246610b724493bf1ce98e0e59c9b7c837e5acfb7/regex-2026.7.19-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:09523a592938aa9f587fb74467c63ff0cf88fc3df14c82ab0f0517dcf76aaa62" }, + { url = "https://mirrors.aliyun.com/pypi/packages/24/57/ccb20b6be5f1f52a053d1ba2a8f7a077edb9d918248b8490d7506c6832b3/regex-2026.7.19-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:1ebac3474b8589fce2f9b225b650afd61448f7c73a5d0255a10cc6366471aed1" }, + { url = "https://mirrors.aliyun.com/pypi/packages/a3/82/f3b263cf8fad927dc102891da8502e718b7ff9d19af7a2a07c03865d7188/regex-2026.7.19-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:4a0530bb1b8c1c985e7e2122e2b4d3aedd8a3c21c6bfddae6767c4405668b56e" }, + { url = "https://mirrors.aliyun.com/pypi/packages/47/2e/1687bd1b6c2aed5e672ccf845fc11557821fe7366d921b50889ea5ce57bf/regex-2026.7.19-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2ef7eeb108c47ce7bcc9513e51bcb1bf57e8f483d52fce68a8642e3527141ae0" }, + { url = "https://mirrors.aliyun.com/pypi/packages/76/7c/cc4e7655181b2d9235b704f2c5e19d8eff002bbc437bae59baee0e381aca/regex-2026.7.19-cp313-cp313t-win32.whl", hash = "sha256:64b6ca7391a1395c2638dd5c7456d67bea44fc6c5e8e92c5dc8aa6a8f23292b4" }, + { url = "https://mirrors.aliyun.com/pypi/packages/bb/14/961b4c7b05a2391c32dbc85e27773076671ef8f97f36cec70fe414734c02/regex-2026.7.19-cp313-cp313t-win_amd64.whl", hash = "sha256:f04b9f56b0e0614c0126be12c2c2d9f8850c1e57af302bd0a63bed379d4af974" }, + { url = "https://mirrors.aliyun.com/pypi/packages/ce/67/795644550d788ddbb6dc458c95895f8009978ea6d6ea76b005eb3f45e8c9/regex-2026.7.19-cp313-cp313t-win_arm64.whl", hash = "sha256:fcee38cd8e5089d6d4f048ba1233b3ad76e5954f545382180889112ff5cb712d" }, + { url = "https://mirrors.aliyun.com/pypi/packages/d2/25/0c4c452f8ef3efe456745b2f33195f5904b573fb4c2ff3f0cb9ec188461e/regex-2026.7.19-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:a81758ed242b861b72e778ba34d41366441a2e10b16b472784c88da2dea7e2dd" }, + { url = "https://mirrors.aliyun.com/pypi/packages/24/9e/b70ca6c1704f6c7cd32a9e143c86cc5968d10981eca284bad670c245ea7d/regex-2026.7.19-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4aa5435cdb3eb6f55fe98a171b05e3fbcd95fadaa4aa32acf62afd9b0cfdbcac" }, + { url = "https://mirrors.aliyun.com/pypi/packages/87/74/0b692da2520d51fbff19c88b83d97e4c702909dd02386c585998b7e2dbed/regex-2026.7.19-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:60be8693a1dadc210bbcbc0db3e26da5f7d01d1d5a3da594e99b4fa42df404f5" }, + { url = "https://mirrors.aliyun.com/pypi/packages/e3/a7/1d478e614016045a33feae57446215f9fd65b665a5ceb2f891fb3183bc52/regex-2026.7.19-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d19662dbedbe783d323196312d38f5ba53cf56296378252171985da6899887d3" }, + { url = "https://mirrors.aliyun.com/pypi/packages/aa/ae/11b9c9411d92c30e3d2db32df5a31133e4a99a8fc397a604fd08f6c4bffb/regex-2026.7.19-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d15df07081d91b76ff20d43f94592ee110330152d617b730fdbe5ef9fb680053" }, + { url = "https://mirrors.aliyun.com/pypi/packages/b1/62/2b2efc4992f91d6d204b24c647c9f9412e85379d92b7c0ab9fdae622327e/regex-2026.7.19-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:56ad4d9f77df871a99e25c37091052a02528ec0eb059de928ee33956b854b45b" }, + { url = "https://mirrors.aliyun.com/pypi/packages/14/71/986ceea9aa3da548bf1357cad89b63915ec6d21ec957c8113b29ece567df/regex-2026.7.19-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7322ec6cc9fba9d49ab888bb82d67ac5625627aa168f0165139b17018df3fb8a" }, + { url = "https://mirrors.aliyun.com/pypi/packages/15/be/ce9d9534b2cda96eab32c548261224b9b4e220a4126f098f60f42ae7b4cd/regex-2026.7.19-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9c7472192ebfad53a6be7c4a8bfb2d64b81c0e93a1fc8c57e1dd0b638297b5d1" }, + { url = "https://mirrors.aliyun.com/pypi/packages/61/2b/58b5c710f2c3929515a25f3a1ca0dad0dcd4518d4fff3cf23bc7adb8dcd2/regex-2026.7.19-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c10b82c2634df08dfb13b1f04e38fe310d086ee092f4f69c0c8da234251e556e" }, + { url = "https://mirrors.aliyun.com/pypi/packages/84/03/5fe091935b74f15fe0f97998c215cae418d1c0413f6258c7d4d2e83aa37f/regex-2026.7.19-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:17ed5692f6acc4183e98331101a5f9e4f64d72fe58b753da4d444a2c77d05b12" }, + { url = "https://mirrors.aliyun.com/pypi/packages/d8/fa/d60bf82e10841eef62a9e32aac401468f05fddfbcb2942e342b1ba3d2433/regex-2026.7.19-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:22a992de9a0d91bda927bf02b94351d737a0302905432c88a53de7c4b9ce62e2" }, + { url = "https://mirrors.aliyun.com/pypi/packages/bf/5d/11e64d151b0662b81d6bf644c74dc118d461df85bdf2577fadbbf751788a/regex-2026.7.19-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:618a0aed532be87294c4477b0481f3aa0f1520f4014a4374dd4cf789b4cd2c97" }, + { url = "https://mirrors.aliyun.com/pypi/packages/7c/34/532efb87488d90807bae6a443d357ee5e2728a478c597619c8aaa17cc0bd/regex-2026.7.19-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2ce9e679f776649746729b6c86382da519ef649c8e34cc41df0d2e5e0f6c36d4" }, + { url = "https://mirrors.aliyun.com/pypi/packages/d6/90/3a8d5ca977171ec3ae21a71207d2228b2663bde14d7f7ef0e6363ecf9290/regex-2026.7.19-cp314-cp314-win32.whl", hash = "sha256:73f272fba87b8ccfe70a137d02a54af386f6d27aa509fbffdd978f5947aae1aa" }, + { url = "https://mirrors.aliyun.com/pypi/packages/96/e1/8862885e70409de70e8c005f57fb2e7be8d9ef0317250d60f4c9660a300d/regex-2026.7.19-cp314-cp314-win_amd64.whl", hash = "sha256:d721e53758b2cca74990185eb0671dd466d7a388a1a45d0c6f4c13cef41a68ac" }, + { url = "https://mirrors.aliyun.com/pypi/packages/08/82/2693e53e29f9104d9de95d37ce4dd826bd32d5f9c0085d3aa6ac042675c4/regex-2026.7.19-cp314-cp314-win_arm64.whl", hash = "sha256:65fa6cb38ed5e9c3637e68e544f598b39c3b86b808ed0627a67b68320384b459" }, + { url = "https://mirrors.aliyun.com/pypi/packages/92/b7/9a01aa16461a18cde9d7b9c3ab21e501db2ce33725f53014342b91df2b0a/regex-2026.7.19-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:5a2721c8720e2cb3c209925dfb9200199b4b07361c9e01d321719404b21458b3" }, + { url = "https://mirrors.aliyun.com/pypi/packages/f3/5e/bbaeca815dc9191c424c94a4fdc5c87c75748a64a6271821212ebdd4e1a3/regex-2026.7.19-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:199535629f25caf89698039af3d1ad5fcae7f933e2112c73f1cdf49165c99518" }, + { url = "https://mirrors.aliyun.com/pypi/packages/cd/d6/0dd1a321afaab95eb7ff44aa0f637301786f1dc71c6b797b9ed236ed8890/regex-2026.7.19-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9b60d7814174f059e5de4ab98271cc5ba9259cfea55273a81544dceea32dc8d9" }, + { url = "https://mirrors.aliyun.com/pypi/packages/92/5f/40bacf91d0904f812e13bbbab3864604c463eced8afdc54aeaa50492ea95/regex-2026.7.19-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dbece16025afda5e3031af0c4059207e61dcf73ef13af844964f57f387d1c435" }, + { url = "https://mirrors.aliyun.com/pypi/packages/94/7c/4902744261f775aeede8b5627314b38482da29cf49a57b66a6fb753246c5/regex-2026.7.19-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d24ecb4f5e009ea0bd275ee37ad9953b32005e2e5e60f8bbae16da0dbbf0d3a0" }, + { url = "https://mirrors.aliyun.com/pypi/packages/16/70/6980c9be6bf21c0a60ed3e0aea39cf419ecf3b08d1d9947bc56e196ef186/regex-2026.7.19-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8cae6fd77a5b72dae505084b1a2ee0360139faf72fedbab667cd7cc65aae7a6a" }, + { url = "https://mirrors.aliyun.com/pypi/packages/52/92/8b2bd872782ce8c42691e39acb38eb8efe014e5ddb78ad7d943d6f197ce9/regex-2026.7.19-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9724e6cb5e478cd7d8cabf027826178739cb18cf0e117d0e32814d479fa02276" }, + { url = "https://mirrors.aliyun.com/pypi/packages/de/2d/33a602f657bdc4041f17d79f92ab18261d255d91a06117a6e29df023e5e2/regex-2026.7.19-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:572fc57b0009c735ee56c175ea021b637a15551a312f56734277f923d6fd0f6c" }, + { url = "https://mirrors.aliyun.com/pypi/packages/9e/36/0987cf4cb271680064a70d24a475873775a151d0b7058698a006cb0cae4a/regex-2026.7.19-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:20568e182eb82d39a6bf7cff3fd58566f14c75c6f74b2c8c96537eecf9010e3a" }, + { url = "https://mirrors.aliyun.com/pypi/packages/a8/24/c14f31c135e1ba55fa4f9a58ca98d0842512bf6188230763c31c8f449e3b/regex-2026.7.19-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:1d58561843f0ff7dc78b4c28b5e2dc388f3eff94ebc8a232a3adba961fc00009" }, + { url = "https://mirrors.aliyun.com/pypi/packages/14/85/181a12211f22469f24d2de1ebddfe397d2396e2c29013b9a58134a91069a/regex-2026.7.19-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:61bb1bd45520aacd56dd80943bd34991fb5350afdd1f36f2282230fd5154a218" }, + { url = "https://mirrors.aliyun.com/pypi/packages/23/58/bd1a0c1a62251366f8d21f41b1ea3c76994962071b8b6ea42f72d505c0f0/regex-2026.7.19-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:cd3584591ea4429026cdb931b054342c2bcf189b44ff367f8d5c15bc092a2966" }, + { url = "https://mirrors.aliyun.com/pypi/packages/e4/4f/f7e2dad6756b2fe1fe75dd90a628c3b45f249d39f948dd90cd2476325417/regex-2026.7.19-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5cc26a66e212fa5d6c6170c3a40d99d888db3020c6fdab1523250d4341382e44" }, + { url = "https://mirrors.aliyun.com/pypi/packages/2b/d7/01d31d5bdb09bc026fab77f59a371fdf8f9b292e4810546c56182ca70498/regex-2026.7.19-cp314-cp314t-win32.whl", hash = "sha256:2c4e61e2e1be56f63ec3cc618aa9e0de81ef6f43d177205451840022e24f5b78" }, + { url = "https://mirrors.aliyun.com/pypi/packages/52/0e/cea4ce73bc0a8247a0748228ae6669984c7e1f8134b6fa66e59c0572e0ea/regex-2026.7.19-cp314-cp314t-win_amd64.whl", hash = "sha256:c639ea314df70a7b2811e8020448c75af8c9445f5a60f8a4ced81c306a9380c2" }, + { url = "https://mirrors.aliyun.com/pypi/packages/6f/b6/26e41975febae63b7a6e3e02f32cff6cff2e4f10d19c929082f56aebf7c6/regex-2026.7.19-cp314-cp314t-win_arm64.whl", hash = "sha256:9a15e785f244f3e07847b984ce8773fc3da10a9f3c131cc49a4c5b4d672b4547" }, +] + [[package]] name = "requests" version = "2.34.2" @@ -1066,6 +1514,19 @@ wheels = [ { url = "https://mirrors.aliyun.com/pypi/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06" }, ] +[[package]] +name = "resend" +version = "2.35.0" +source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } +dependencies = [ + { name = "requests" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/f0/b9/e07f2f2bd992ef4565b9c315dfd46b3df66ce89df1d928f442b9b39cbe3b/resend-2.35.0.tar.gz", hash = "sha256:26ced7b22cbd89f7b8c7ba9719d0708ab10c06ddd0f91ba6ec861f3a328101b3" } +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/cc/92/1c0912b68ae082a55dfdd32e4b5117905ae9fd1b6efc5f5e7c69a4ee6894/resend-2.35.0-py2.py3-none-any.whl", hash = "sha256:cd75299d626f4735af52910989b3f51919032ef316e2d60563c79c319e7b24a6" }, +] + [[package]] name = "rich" version = "15.0.0" @@ -1167,6 +1628,18 @@ wheels = [ { url = "https://mirrors.aliyun.com/pypi/packages/ec/bb/2799cc2ede3ed41131f8975621e7213dfc7ef4acbbaadfa440f32500c370/starlette-1.3.1-py3-none-any.whl", hash = "sha256:c7372aae11c3c3f26a42df7bd626cec2f47d03483d261d369516a615a53714c6" }, ] +[[package]] +name = "sympy" +version = "1.14.0" +source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } +dependencies = [ + { name = "mpmath" }, +] +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/83/d3/803453b36afefb7c2bb238361cd4ae6125a569b4db67cd9e79846ba2d68c/sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517" } +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5" }, +] + [[package]] name = "tenacity" version = "9.1.4" @@ -1176,6 +1649,65 @@ wheels = [ { url = "https://mirrors.aliyun.com/pypi/packages/d7/c1/eb8f9debc45d3b7918a32ab756658a0904732f75e555402972246b0b8e71/tenacity-9.1.4-py3-none-any.whl", hash = "sha256:6095a360c919085f28c6527de529e76a06ad89b23659fa881ae0649b867a9d55" }, ] +[[package]] +name = "tiktoken" +version = "0.13.0" +source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } +dependencies = [ + { name = "regex" }, + { name = "requests" }, +] +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/e4/e5/5f3cb2159769d0f4324c0e9e87f9de3c4b1cd45848a96b2eb3566ad5ca77/tiktoken-0.13.0.tar.gz", hash = "sha256:c9435714c3a84c2319499de9a300c0e604449dd0799ff246458b3bb6a7f433c1" } +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/85/8e/144bde4e01df66b34bb865557c7cd754ed08b036217ebd79c9db5e9048a9/tiktoken-0.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:32ac870a806cfb260a02d0cb70426aef02e038297f8ad50df5040bb5af360791" }, + { url = "https://mirrors.aliyun.com/pypi/packages/36/18/d4ac9d20956cdebca04841316660ed584c2fecdc2b81722a28bc7ad3b1e4/tiktoken-0.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4d9980f11429ed2d737c463bb1fb78cf330caa026adf002f714aced7849a687b" }, + { url = "https://mirrors.aliyun.com/pypi/packages/74/ed/6bb8d05b9f731f749fee5c6f5ca63e981143c826a5985877330507bd13b7/tiktoken-0.13.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:3f277ebea5edd7b8bf03c6f9431e1d67d517530115572b2dc1d465326e8f88c7" }, + { url = "https://mirrors.aliyun.com/pypi/packages/34/de/2ca96b07a82d972b74fe4b46de055b79c904e45c7eab699354a0bfa697dc/tiktoken-0.13.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:a116178fa7e1b4065bff05214360373a65cac22f965be7b3f73d00a0dbfe7649" }, + { url = "https://mirrors.aliyun.com/pypi/packages/ee/dc/9dafec002c2d4424378563cf4cf5c7fb93631d2a55013c8b87554ee4012c/tiktoken-0.13.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2c397ddda233208345b01bd30f2fca79ff730e55731d0108a603f9bc57f6af3b" }, + { url = "https://mirrors.aliyun.com/pypi/packages/a1/d0/1f8578c45b2f24759b46f0b50d31878c63c73e6bf0f2227e10ec5c5408dc/tiktoken-0.13.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:95097e4f89b06403976e498abf61a0ee73a7497e73fb599cb211d8197a054d91" }, + { url = "https://mirrors.aliyun.com/pypi/packages/aa/90/28d7f154888610aa9237e541986beb62b479df29d193a5a0617dbb1514d0/tiktoken-0.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:8f2d16e7a7c783ad81f36e457d046d1f1c8af70b22aec8a13238efe531977c41" }, + { url = "https://mirrors.aliyun.com/pypi/packages/9c/83/b096c859c2a47c11731bf2f5885f4028b809dfe2396582883eed9cae372f/tiktoken-0.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5df5d1507bd245f1ccad4a074698240021239e455eb0bb4ced4e3d7181872154" }, + { url = "https://mirrors.aliyun.com/pypi/packages/53/61/c68e123b6d753e3fc2751e9b18e732c9d8bf1e1926762e736eee935d931c/tiktoken-0.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8fe806a50664e83a6ffd56cbd1e4f5dcc6cd32a3e7538f70dc38b1a271384545" }, + { url = "https://mirrors.aliyun.com/pypi/packages/ef/8b/96cc178cc584e65d363134500f297790b06cd48cdeb1e8fcf7bbe60f4715/tiktoken-0.13.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:125bc05005e747f993a83dc67934249932d6e4209854452cd4c0b1d53fba3ba2" }, + { url = "https://mirrors.aliyun.com/pypi/packages/86/f5/bab735d2c72ea55404b295d02d092644eb5f7cc6205e34d35eb9abfb9ab2/tiktoken-0.13.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:5e6358911cab4adee6712da27d65573496a4f68cf8a2b5fca6a4ad10fc5748cf" }, + { url = "https://mirrors.aliyun.com/pypi/packages/4e/b9/6de04ebdf904edfaad87788011b3735087a0c9ea671b9027e1e4e965e8c8/tiktoken-0.13.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:975cbd78d085d75d26b59660e262736dcaed1e35f8f142cd6291025c01d25486" }, + { url = "https://mirrors.aliyun.com/pypi/packages/0d/9c/470a05f3b1caf038f44880e334d47ab674e0c80d514c66b375d14d5afa10/tiktoken-0.13.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:75ab9bc99fa020a4c283424590ecd7f3afd70c1c281cb3fa3192a6c3af9f9615" }, + { url = "https://mirrors.aliyun.com/pypi/packages/42/a6/c1936d16055436cb32e6c6128d68629622e00f4768562f55653752d34768/tiktoken-0.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:6b1615f0ff71953d19729ceb18865429c185b0a23c5353f1bbca34a394bf60f7" }, + { url = "https://mirrors.aliyun.com/pypi/packages/d6/07/acb5992c3772b5a36284f742cfb7a5895aa4471d1848ac31464ad50d7fdf/tiktoken-0.13.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6eb4a5bfbc6426938026b1a334e898ac53541360d62d8c689870160cc80abd67" }, + { url = "https://mirrors.aliyun.com/pypi/packages/14/e9/742e9aec30f59b9f161f7ff7cd072e02ea836c9e1c0854a8076dfcd40d5c/tiktoken-0.13.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:43cee3e5400573b2046fbf092cc7a5bc30164f9e4c95ce20714da929df48737a" }, + { url = "https://mirrors.aliyun.com/pypi/packages/72/74/ca1541b053e7648254d2e4b42a253e1bb4359f2c91a0a8d49228c794e1a0/tiktoken-0.13.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:7de52e3f566d19b3b11bd37eea552c6c305ad74081f736882bd44d148ed4c48d" }, + { url = "https://mirrors.aliyun.com/pypi/packages/46/e3/93825eaf5a4a504795b787e5d5dea07fbeb3dabf97aa7b450be8bde59c89/tiktoken-0.13.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:51384448aa508e4df84c0f7c1dc3211c7f7b8096325660ee5fc82f3e11b381ce" }, + { url = "https://mirrors.aliyun.com/pypi/packages/8c/46/002b68de6827091d5ae90b048f326e8aad8d953520950e5ce1508879414f/tiktoken-0.13.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e28157350f7ebf35008dd8e9e0fdb621f976e4230c881099c85e8cf07eaa50e2" }, + { url = "https://mirrors.aliyun.com/pypi/packages/db/c6/d393e3185a276505182f7abd93fe714f3c444a2be9180798fa052347504e/tiktoken-0.13.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:165cf1820ea4a354985c2490a5205d4cc74661c934aca79dd0368232fff94e0f" }, + { url = "https://mirrors.aliyun.com/pypi/packages/b7/4d/bc07d1f1635d4897a202acc0ae11c2886eaa7325c359ba4741b47bf8e225/tiktoken-0.13.0-cp313-cp313t-win_amd64.whl", hash = "sha256:6c43a675ca14f6f2749ba7f12075d37456015a24b859f2517b9beb4ef30807ec" }, + { url = "https://mirrors.aliyun.com/pypi/packages/8c/93/0dd6adca026a616c3a92974566b43381eea4b475ce1f36c062b8271a9ac5/tiktoken-0.13.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaaaef47c2406277181d2086484c317bf7fc433e2d5d03ff94f56b0dcec87471" }, + { url = "https://mirrors.aliyun.com/pypi/packages/d9/77/5ec6e6bc5b30bed6d93f7f2162d8f6b32437b3ba27cb527cfe004f6109c9/tiktoken-0.13.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ca8b310bd93b3772cb1b7922d915446864860f562bdfe4825c63a0aed3fb28cd" }, + { url = "https://mirrors.aliyun.com/pypi/packages/94/b0/c8ae9aff00d625c50659b4513e707a0462c4bf5d4d6cc1b802103225c02e/tiktoken-0.13.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:32e0c12305105002c047b3bb1070b0dd9a73b0cb3b2856a8972b810e7a4f5881" }, + { url = "https://mirrors.aliyun.com/pypi/packages/1b/ac/6a5dddd1d0a6018ecb389bd0353e6b4a515eb4d2286611bd0ace1937b9e1/tiktoken-0.13.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:5ba5fd62507a932d1241346179e3b39bc7bf7408f03c272652d93b3bedf5db24" }, + { url = "https://mirrors.aliyun.com/pypi/packages/f4/b8/585032b4384b2f7dcdaddcb52865c83a701a420d09e3c2b4a2be1c450c57/tiktoken-0.13.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d108bc2d470fc53c8ecd24f2c0fd2b5f98c33e87cdb6aa2e9b8c5dced703d273" }, + { url = "https://mirrors.aliyun.com/pypi/packages/cd/b6/993ff1ded3958215fd341a847b8e5ffeb5de473f435296870d314fc91ac4/tiktoken-0.13.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:cb99cb5127449f58d0a2d5f5ccfb390d8dbdfd919c221246caaee29d8725ed51" }, + { url = "https://mirrors.aliyun.com/pypi/packages/dd/3d/fef7e06e3b33e7538db0ced734cf9fe23b6832d2ac4990c119c377aec55e/tiktoken-0.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:115c4f26ffa11caac8b54eea35c2ad38c612c20a48d35dd15d70a02ac6f51f58" }, + { url = "https://mirrors.aliyun.com/pypi/packages/c1/82/a7fc44582bc32ab00de988a2299bf77c077f59068b233109e34b7d6ca7e6/tiktoken-0.13.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:472527e9132952f2fbf77cd290658bacf003d4d5a3fabc18e5fbd407cbae4d9b" }, + { url = "https://mirrors.aliyun.com/pypi/packages/37/d0/24d8a890c14f432a05cea669c17bebeaa99f96a7c79523b590f564246411/tiktoken-0.13.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:4e2f67d27c9626cdd25fe33d9313c5cdb3d8d82da646b68d6eb8e7e9c20e6448" }, + { url = "https://mirrors.aliyun.com/pypi/packages/49/b7/2ab43f62788a9266187a9bfc1d3af99ad83e5eaa25fbef168a69cd5ad14f/tiktoken-0.13.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:2b920b35805cd64585a37c3dc7ce65fba4d2d36016be01e1d7942482ca29093a" }, + { url = "https://mirrors.aliyun.com/pypi/packages/64/39/1494321ed323ce7a14d88e3cd6cb9058625977df1c6961ddc492bd10a9f3/tiktoken-0.13.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:493af3aa28a4aaf2e3d2600a2ee717252c9bf5ab38fff94eb5a02db5ab77e5ad" }, + { url = "https://mirrors.aliyun.com/pypi/packages/96/d9/dfd086aa2d918c563a140720e0ce296cada1634efd2783d5cf51e05f984e/tiktoken-0.13.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6644c9c2b5cf3916f5a3641d7d12fdb3f006a7b3d9ff6acdaec44e29ab1ff91e" }, + { url = "https://mirrors.aliyun.com/pypi/packages/2f/68/a18b4f307086954fdae32714cb4f85562e34f9d34ab206e61f1816aa6018/tiktoken-0.13.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5cb65b60b9408563676d874a3a4ee573370066f0dc4e29d84e82e989c6517424" }, + { url = "https://mirrors.aliyun.com/pypi/packages/16/5b/f2aa703a4fc5d2dff73460a7d46cc2f3f44aa0f3dd8eeb20d2a0ecf68862/tiktoken-0.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:85b78cc3a2c3d48723ca751fa981f1fedccd54194ca0471b957364353a898b07" }, +] + +[[package]] +name = "tqdm" +version = "4.70.0" +source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/21/3b/6c24bec5be5e743ffd99576daa5cc077722fc7d5bbc00bd133fa0c698dc6/tqdm-4.70.0.tar.gz", hash = "sha256:55b0b0dbd97462d06ebee91e4dac24ed4d4702be82b24f07e6c1d27e08cea220" } +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/f9/1c/01bfd571a64e7f270e6bab5e33777debe0edc56759233ce84f27dec92d14/tqdm-4.70.0-py3-none-any.whl", hash = "sha256:7f585706bfddbdebf89daac705b2dfcc16890130727d3197ca62c732b4310953" }, +] + [[package]] name = "typing-extensions" version = "4.16.0" @@ -1458,6 +1990,8 @@ name = "windup-ai-engine" version = "0.1.0" source = { editable = "packages/ai_engine" } dependencies = [ + { name = "av" }, + { name = "imageio" }, { name = "langchain-core" }, { name = "langgraph" }, { name = "numpy" }, @@ -1468,6 +2002,8 @@ dependencies = [ [package.metadata] requires-dist = [ + { name = "av", specifier = ">=14.0" }, + { name = "imageio", specifier = ">=2.36" }, { name = "langchain-core", specifier = ">=0.3" }, { name = "langgraph", specifier = ">=0.2" }, { name = "numpy", specifier = ">=1.26" }, @@ -1482,7 +2018,7 @@ version = "0.1.0" source = { editable = "packages/app" } dependencies = [ { name = "fastapi" }, - { name = "pydantic" }, + { name = "pydantic", extra = ["email"] }, { name = "python-multipart" }, { name = "sqlalchemy" }, { name = "uvicorn", extra = ["standard"] }, @@ -1494,7 +2030,7 @@ dependencies = [ [package.metadata] requires-dist = [ { name = "fastapi", specifier = ">=0.115" }, - { name = "pydantic", specifier = ">=2.7" }, + { name = "pydantic", extras = ["email"], specifier = ">=2.7" }, { name = "python-multipart", specifier = ">=0.0.9" }, { name = "sqlalchemy", specifier = ">=2.0" }, { name = "uvicorn", extras = ["standard"], specifier = ">=0.30" }, @@ -1520,10 +2056,19 @@ version = "0.1.0" source = { editable = "packages/framework" } dependencies = [ { name = "httpx" }, + { name = "langchain-core" }, + { name = "langchain-openai" }, + { name = "numpy" }, + { name = "onnxruntime" }, + { name = "passlib", extra = ["bcrypt"] }, + { name = "pillow" }, { name = "psycopg", extra = ["binary"] }, { name = "pydantic" }, { name = "pydantic-settings" }, { name = "pyjwt" }, + { name = "qiniu" }, + { name = "redis" }, + { name = "resend" }, { name = "sqlalchemy" }, { name = "windup-common" }, ] @@ -1531,10 +2076,19 @@ dependencies = [ [package.metadata] requires-dist = [ { name = "httpx", specifier = ">=0.27" }, + { name = "langchain-core", specifier = ">=0.3" }, + { name = "langchain-openai", specifier = ">=0.3" }, + { name = "numpy", specifier = ">=1.26" }, + { name = "onnxruntime", specifier = ">=1.17,<1.24" }, + { name = "passlib", extras = ["bcrypt"], specifier = ">=1.7" }, + { name = "pillow", specifier = ">=10.4" }, { name = "psycopg", extras = ["binary"], specifier = ">=3.2" }, { name = "pydantic", specifier = ">=2.7" }, { name = "pydantic-settings", specifier = ">=2.4" }, { name = "pyjwt", specifier = ">=2.9" }, + { name = "qiniu", specifier = ">=7.14" }, + { name = "redis", specifier = ">=5.0" }, + { name = "resend", specifier = ">=2.0" }, { name = "sqlalchemy", specifier = ">=2.0" }, { name = "windup-common", editable = "packages/common" }, ] diff --git a/db/init.sql b/db/init.sql new file mode 100644 index 00000000..cee115ee --- /dev/null +++ b/db/init.sql @@ -0,0 +1,128 @@ +-- ────────────────────────────────────────────────────────────── +-- Windup 数据库初始化脚本 +-- 首次启动时自动执行,创建所有表结构 +-- 数据库已通过 POSTGRES_DB 环境变量自动创建 +-- ────────────────────────────────────────────────────────────── + +/*项目表,全局约束角色人物。*/ +CREATE TABLE windup_project ( + id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + user_id BIGINT NOT NULL, + workflow_id BIGINT, + project_name varchar(20) NOT NULL, + character_perspective SMALLINT NOT NULL, + directional_movement SMALLINT NOT NULL, + sprite_width SMALLINT NOT NULL, + sprite_height SMALLINT NOT NULL, + game_style TEXT, + sprite_sample_url TEXT, + create_at TIMESTAMPTZ, + update_at TIMESTAMPTZ, + + CONSTRAINT uq_windup_project_user_name UNIQUE (user_id, project_name) +); + +COMMENT ON TABLE windup_project IS '项目表'; +COMMENT ON COLUMN windup_project.id IS '项目表主键'; +COMMENT ON COLUMN windup_project.user_id IS '创建者 ID'; +COMMENT ON COLUMN windup_project.workflow_id IS '工作流 ID'; +COMMENT ON COLUMN windup_project.project_name IS '项目名称'; +COMMENT ON COLUMN windup_project.character_perspective IS '游戏视角 :1 = 横版视角,2 = 俯视 ,3 = 2.5D '; +COMMENT ON COLUMN windup_project.directional_movement IS '移动方向 :1 = 单向 ,2 = 四向,3 = 八向'; +COMMENT ON COLUMN windup_project.sprite_width IS '角色尺寸 宽:32 、64 、128、256、512、1024、2048'; +COMMENT ON COLUMN windup_project.sprite_height IS '角色尺寸 高:32 、64 、128、256、512、1024、2048'; +COMMENT ON COLUMN windup_project.game_style IS '游戏风格'; +COMMENT ON COLUMN windup_project.sprite_sample_url IS '参考图URL'; +COMMENT ON COLUMN windup_project.create_at IS '创建时间'; +COMMENT ON COLUMN windup_project.update_at IS '修改时间'; + +/*用户表*/ +CREATE TABLE windup_user ( + id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + email VARCHAR(255) UNIQUE, + password_hash VARCHAR(255) NOT NULL, + nickname VARCHAR(255), + email_verified_at TIMESTAMPTZ, + status SMALLINT DEFAULT 0, + last_login_at TIMESTAMPTZ, + create_at TIMESTAMPTZ, + update_at TIMESTAMPTZ +); + +COMMENT ON TABLE windup_user IS '用户表'; +COMMENT ON COLUMN windup_user.id IS '用户表主键'; +COMMENT ON COLUMN windup_user.email IS '邮箱地址'; +COMMENT ON COLUMN windup_user.password_hash IS '用户密码(加密存储)'; +COMMENT ON COLUMN windup_user.nickname IS '用户昵称'; +COMMENT ON COLUMN windup_user.email_verified_at IS '邮箱校验时间'; +COMMENT ON COLUMN windup_user.status IS '用户状态 默认0 正常,1 封禁'; +COMMENT ON COLUMN windup_user.last_login_at IS '上次登录时间'; +COMMENT ON COLUMN windup_user.create_at IS '创建时间'; +COMMENT ON COLUMN windup_user.update_at IS '修改时间'; + +/*角色资产表*/ +CREATE TABLE windup_character ( + id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + project_id BIGINT NOT NULL, + name varchar(20), + description TEXT NULL, + reference_image_url TEXT NULL, + character_data JSONB NOT NULL DEFAULT '{}'::jsonb, + status SMALLINT NOT NULL DEFAULT 1, + create_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + update_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT ck_windup_character_status CHECK (status IN (0, 1)), + CONSTRAINT ck_windup_character_data_object CHECK (jsonb_typeof(character_data) = 'object') +); + +CREATE INDEX idx_windup_character_project_id + ON windup_character (project_id); + +CREATE INDEX idx_windup_character_project_update_at + ON windup_character (project_id, update_at DESC); + +COMMENT ON TABLE windup_character IS '角色资产表;角色隶属于项目,是资产库中的基本资产'; +COMMENT ON COLUMN windup_character.id IS '角色资产主键'; +COMMENT ON COLUMN windup_character.project_id IS '所属项目 ID'; +COMMENT ON COLUMN windup_character.name IS '角色名称'; +COMMENT ON COLUMN windup_character.description IS '角色描述'; +COMMENT ON COLUMN windup_character.reference_image_url IS '角色参考图 URL;角色模板仅作为角色的参考图属性,不单独建表'; +COMMENT ON COLUMN windup_character.character_data IS '角色完整数据 JSON;包含造型、动作及动作帧等信息'; +COMMENT ON COLUMN windup_character.status IS '角色状态: 1-正常, 0-禁用'; +COMMENT ON COLUMN windup_character.create_at IS '创建时间'; +COMMENT ON COLUMN windup_character.update_at IS '最后更新时间'; +COMMENT ON CONSTRAINT ck_windup_character_status ON windup_character IS '角色状态只能为 0 或 1'; +COMMENT ON CONSTRAINT ck_windup_character_data_object ON windup_character IS '角色完整数据必须为 JSON 对象'; + +/*生成任务表*/ +CREATE TABLE IF NOT EXISTS windup_generation_task ( + id BIGSERIAL PRIMARY KEY, + user_id BIGINT NOT NULL, + project_id BIGINT, + task_type TEXT NOT NULL DEFAULT 'character_image', + status TEXT NOT NULL DEFAULT 'pending', + input_payload JSONB NOT NULL DEFAULT '{}', + result_type TEXT, + result JSONB, + error_message TEXT, + create_at TIMESTAMPTZ NOT NULL DEFAULT now(), + update_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + +COMMENT ON TABLE windup_generation_task IS '生成任务表——记录每次AI生成任务'; +COMMENT ON COLUMN windup_generation_task.id IS '任务主键'; +COMMENT ON COLUMN windup_generation_task.user_id IS '发起用户 ID'; +COMMENT ON COLUMN windup_generation_task.project_id IS '关联项目 ID'; +COMMENT ON COLUMN windup_generation_task.task_type IS '任务类型:character_image = 角色图片生成'; +COMMENT ON COLUMN windup_generation_task.status IS '任务状态:pending = 待执行,running = 执行中,completed = 完成,failed = 失败'; +COMMENT ON COLUMN windup_generation_task.input_payload IS '任务输入参数 JSON'; +COMMENT ON COLUMN windup_generation_task.result_type IS '结果类型'; +COMMENT ON COLUMN windup_generation_task.result IS '任务结果 JSON'; +COMMENT ON COLUMN windup_generation_task.error_message IS '错误信息(失败时记录)'; +COMMENT ON COLUMN windup_generation_task.create_at IS '创建时间'; +COMMENT ON COLUMN windup_generation_task.update_at IS '修改时间'; + +CREATE INDEX IF NOT EXISTS idx_generation_task_user_id ON windup_generation_task (user_id); +CREATE INDEX IF NOT EXISTS idx_generation_task_project_id ON windup_generation_task (project_id); +CREATE INDEX IF NOT EXISTS idx_generation_task_status ON windup_generation_task (status); diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..ab4a7ae6 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,98 @@ +# ── Windup Docker Compose ───────────────────────────────────────────── +# 使用方式: +# 启动服务: docker compose up -d +# 查看日志: docker compose logs -f [service] +# 停止服务: docker compose down +# 清理数据: docker compose down -v (⚠️ 会删除数据库数据) +# 重新构建: docker compose up -d --build + +services: + # ── Redis 缓存(验证码 / refresh_token) ── + redis: + image: redis:7-alpine + container_name: windup-redis + restart: unless-stopped + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 5s + retries: 5 + networks: + - windup-net + + # ── PostgreSQL 数据库 ── + postgres: + image: postgres:16-alpine + container_name: windup-postgres + restart: unless-stopped + environment: + POSTGRES_USER: ${POSTGRES_USER:-root} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-W1ndup@2026!Secure} + POSTGRES_DB: ${POSTGRES_DB:-windup} + ports: + - "${POSTGRES_EXTERNAL_PORT:-7856}:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + - ./db/init.sql:/docker-entrypoint-initdb.d/init.sql:ro + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-root} -d ${POSTGRES_DB:-windup}"] + interval: 10s + timeout: 5s + retries: 5 + networks: + - windup-net + + # ── 后端 API 服务 ── + backend: + build: + context: ./backend + dockerfile: Dockerfile + container_name: windup-backend + restart: unless-stopped + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + environment: + # Redis(验证码 / refresh_token) + REDIS_URL: redis://redis:6379/0 + # 数据库连接(容器内部通信用 postgres 主机名,端口 5432) + POSTGRES_HOST: postgres + POSTGRES_PORT: 5432 + POSTGRES_USER: ${POSTGRES_USER:-root} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-admin123} + POSTGRES_DB: ${POSTGRES_DB:-windup} + # LLM 配置 + LLM_API_KEY: ${LLM_API_KEY} + LLM_MODEL_ID: ${LLM_MODEL_ID:-doubao-seed-2.0-mini} + LLM_BASE_URL: ${LLM_BASE_URL:-https://api.qnaigc.com/v1} + LLM_IMAGE_MODEL_ID: ${LLM_IMAGE_MODEL_ID:-gemini-3.0-pro-image-preview} + LLM_VIDEO_MODEL_ID: ${LLM_VIDEO_MODEL_ID} + # 搜索 + SERPAPI_API_KEY: ${SERPAPI_API_KEY} + # 七牛云存储 + QINIU_ACCESS_KEY: ${QINIU_ACCESS_KEY} + QINIU_SECRET_KEY: ${QINIU_SECRET_KEY} + QINIU_BUCKET_NAME: ${QINIU_BUCKET_NAME} + QINIU_BUCKET_DOMAIN: ${QINIU_BUCKET_DOMAIN} + QINIU_PRIVATE_SPACE: ${QINIU_PRIVATE_SPACE:-false} + # AI Provider + AI_BASE_URL: ${AI_BASE_URL:-https://api.qnaigc.com/v1} + AI_API_KEY: ${AI_API_KEY} + ports: + - "${WINDUP_PORT:-8000}:8000" + networks: + - windup-net + +volumes: + postgres_data: + driver: local + +networks: + windup-net: + driver: bridge + # 宿主机链路 MTU 是 1480(eno1),compose 自建网络不会继承 daemon 的 mtu 设置, + # 默认仍是 1500 → 大包被丢,表现为 TLS 握手超时(七牛上传域名连不上、pip 下载卡死)。 + driver_opts: + com.docker.network.driver.mtu: "1450"