diff --git a/.gitignore b/.gitignore index 6de0b26d..61dcaf3a 100644 --- a/.gitignore +++ b/.gitignore @@ -30,5 +30,9 @@ output/ .pytest_cache/ .import_linter_cache/ +# 测试覆盖率 +.coverage +htmlcov/ + # 本地数据库初始化脚本 init.sql 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 d3ad65eb..bbea356e 100644 --- a/backend/packages/app/src/windup_app/bootstrap/app.py +++ b/backend/packages/app/src/windup_app/bootstrap/app.py @@ -10,13 +10,20 @@ 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_framework.db import Base, engine +# 模型导入:触发 Base.metadata 注册,确保 create_all 能发现所有表 +from windup_app.server.character.model import Character # noqa: F401 +from windup_app.server.project.model import Project # noqa: F401 +from windup_app.server.user.model import User # noqa: F401 +from windup_app.web.api.auth import router as auth_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.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: @@ -54,7 +61,8 @@ def print_banner() -> None: @asynccontextmanager async def _lifespan(app: FastAPI): - """应用启动时打印 banner,关闭时无特殊处理。""" + """应用启动时建表 + 打印 banner,关闭时无特殊处理。""" + Base.metadata.create_all(engine) print_banner() yield @@ -69,6 +77,10 @@ def create_app() -> FastAPI: allow_methods=["*"], allow_headers=["*"], ) + # 中间件(add_middleware 后加的先执行:请求先进 RateLimit → 再进 Auth → 最后到路由) + app.add_middleware(AuthMiddleware) + app.add_middleware(RateLimitMiddleware) + app.include_router(auth_router) app.include_router(media_router) app.include_router(generation_router) register_exception_handlers(app) @@ -76,10 +88,7 @@ def create_app() -> FastAPI: def main() -> None: - """开发启动入口:用 uvicorn 跑 ``create_app``。 - - host/port/reload 可用 ``WINDUP_HOST`` / ``WINDUP_PORT`` / ``WINDUP_RELOAD`` 覆盖。 - """ + """开发启动入口:用 uvicorn 跑 ``create_app``。""" import uvicorn uvicorn.run( @@ -91,6 +100,5 @@ def main() -> None: ) - if __name__ == "__main__": - 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..31b51464 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 @@ -95,29 +134,33 @@ class LoginByCodeInput: @dataclass -class OAuthCallbackInput: - """OAuth 回调入参。""" +class ChangePasswordInput: + """修改密码入参。""" - provider: str # "github" / "google" - code: str - state: str # CSRF 防护 + old_password: str + new_password: str @dataclass -class ChangePasswordInput: - """修改密码入参。""" +class UpdateNicknameInput: + """修改昵称入参。""" - old_password: str + nickname: str + + +@dataclass +class ResetPasswordInput: + """重置密码入参。""" + + email: str + code: str new_password: str @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..aea85033 --- /dev/null +++ b/backend/packages/app/src/windup_app/server/user/service.py @@ -0,0 +1,510 @@ +"""用户领域服务的 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, + ResetPasswordInput, + UpdateNicknameInput, + 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}" +LOGIN_FAIL_KEY = "login:fail:{email}" +LOGIN_LOCK_KEY = "login:lock:{email}" + +VERIFY_CODE_TTL = 300 # 5 分钟 +COOLDOWN_TTL = 60 # 60 秒 + +LOGIN_FAIL_LIMIT = 5 # 连续错误密码上限 +LOGIN_FAIL_WINDOW = 15 * 60 # 失败计数窗口 15 分钟 +LOGIN_LOCK_DURATION = 15 * 60 # 锁定时长 15 分钟 + + +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 _check_login_lock(self, email: str) -> None: + """检查账号是否因连续错误密码被锁定。""" + lock_key = LOGIN_LOCK_KEY.format(email=email) + if self.redis.get(lock_key): + raise BizException("邮箱或密码错误", code=BizCode.BAD_REQUEST) + + def _record_login_failure(self, email: str) -> None: + """记录一次错误密码,达到上限时锁定账号。""" + fail_key = LOGIN_FAIL_KEY.format(email=email) + count = self.redis.incr(fail_key) + if count == 1: + self.redis.expire(fail_key, LOGIN_FAIL_WINDOW) + if count >= LOGIN_FAIL_LIMIT: + lock_key = LOGIN_LOCK_KEY.format(email=email) + self.redis.setex(lock_key, LOGIN_LOCK_DURATION, "1") + + def _clear_login_failures(self, email: str) -> None: + """登录成功,清除失败计数和锁定。""" + self.redis.delete(LOGIN_FAIL_KEY.format(email=email)) + self.redis.delete(LOGIN_LOCK_KEY.format(email=email)) + + # -- 登录 ------------------------------------------------------------ + + 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._check_login_lock(input.email) + + user = session.scalar(select(User).where(User.email == input.email)) + if user is None: + self._record_login_failure(input.email) + raise BizException("邮箱或密码错误", code=BizCode.BAD_REQUEST) + + if not _verify_password(input.password, user.password_hash): + self._record_login_failure(input.email) + raise BizException("邮箱或密码错误", code=BizCode.BAD_REQUEST) + + if user.status == UserStatus.BANNED: + raise BizException("账号已被封禁", code=BizCode.BAD_REQUEST) + + # 登录成功,清除失败计数 + self._clear_login_failures(input.email) + + # 更新最后登录时间 + 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 reset_password_with_session( + self, session: Session, input: ResetPasswordInput + ) -> None: + """邮箱+验证码重置密码(忘记密码场景)。""" + # 校验验证码(purpose 必须为 reset_password) + self._verify_code(input.email, input.code, "reset_password") + + user = session.scalar(select(User).where(User.email == input.email)) + if user is None: + raise BizException("用户不存在", code=BizCode.NOT_FOUND) + + if user.status == UserStatus.BANNED: + 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 email=%s", user.id, user.email) + + # -- 昵称 ------------------------------------------------------------ + + def update_nickname_with_session( + self, session: Session, user_id: int, input: UpdateNicknameInput + ) -> UserView: + """修改昵称(带 session)。""" + user = session.get(User, user_id) + if user is None: + raise BizException("用户不存在", code=BizCode.NOT_FOUND) + + user.nickname = input.nickname + session.flush() + + logger.info("[WINDUP] 昵称已修改 | user_id=%s", user_id) + return _to_view(user) + + # -- 查询 ------------------------------------------------------------ + + 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..b196a024 --- /dev/null +++ b/backend/packages/app/src/windup_app/web/api/auth.py @@ -0,0 +1,248 @@ +"""认证 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 ResetPasswordInput, UpdateNicknameInput, 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 + + +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 UpdateNicknameRequest(BaseModel): + """修改昵称请求。""" + + nickname: str = Field(min_length=1, max_length=50) + + +class ResetPasswordRequest(BaseModel): + """重置密码请求(忘记密码场景)。""" + + email: EmailStr + code: str = Field(min_length=6, max_length=6, description="reset_password 用途的验证码") + 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})(), + ) + 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="密码修改成功") + + +@router.post("/reset-password", response_model=Response[None]) +def reset_password(body: ResetPasswordRequest, session: Session = Depends(get_session)): + """邮箱+验证码重置密码(忘记密码)。""" + service.reset_password_with_session( + session, + ResetPasswordInput(email=body.email, code=body.code, new_password=body.new_password), + ) + return Response.success(None, message="密码重置成功") + + +@router.patch("/profile", response_model=Response[UserOut]) +def update_nickname(body: UpdateNicknameRequest, request: Request, session: Session = Depends(get_session)): + """修改当前用户昵称。""" + current_user = request.state.current_user + user_view = service.update_nickname_with_session( + session, current_user.id, UpdateNicknameInput(nickname=body.nickname) + ) + return Response.success( + UserOut( + id=user_view.id, + email=user_view.email, + nickname=user_view.nickname, + email_verified_at=user_view.email_verified_at.isoformat() if user_view.email_verified_at else None, + status=user_view.status, + ), + 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..7141d87a --- /dev/null +++ b/backend/packages/app/src/windup_app/web/middleware/auth.py @@ -0,0 +1,84 @@ +"""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.exceptions import BizException +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 " 前缀 + + # 解码 + 验证 + try: + payload = decode_token(token) + except BizException as e: + return _biz_error(e.message, e.code) + + 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..11a05733 --- /dev/null +++ b/backend/packages/app/src/windup_app/web/middleware/ratelimit.py @@ -0,0 +1,148 @@ +"""接口限流中间件。 + +基于 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", + "/auth/reset-password", +} + +# -- Redis key 模板 ------------------------------------------------------ + +RATELIMIT_API_KEY = "ratelimit:api:{ip}" +RATELIMIT_SENSITIVE_KEY = "ratelimit:sensitive:{ip}" +RATELIMIT_USER_KEY = "ratelimit:api:{user_id}" + + +# 可信代理列表:只有这些来源的请求才信任 X-Forwarded-For +TRUSTED_PROXIES: set[str] = {"127.0.0.1", "::1", "172.16.0.0/12"} + + +def _is_trusted_proxy(host: str | None) -> bool: + """判断请求来源是否在可信代理列表中。""" + if not host: + return False + if host in TRUSTED_PROXIES: + return True + # Docker 网段 172.16.0.0/12 + try: + parts = host.split(".") + if len(parts) == 4 and parts[0] == "172" and 16 <= int(parts[1]) <= 31: + return True + except (ValueError, IndexError): + pass + return False + + +def _get_client_ip(request: Request) -> str: + """获取客户端 IP,仅在可信代理后才信任 X-Forwarded-For。""" + client_host = request.client.host if request.client else None + if _is_trusted_proxy(client_host): + forwarded = request.headers.get("x-forwarded-for") + if forwarded: + return forwarded.split(",")[0].strip() + return client_host or "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/pyproject.toml b/backend/pyproject.toml index eef69c05..01029a56 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -14,6 +14,7 @@ windup-app = { workspace = true } dev = [ "import-linter>=2.0", "pytest>=8.0", + "pytest-cov>=5.0", "ruff>=0.6", ] @@ -40,3 +41,7 @@ name = "入口层不经 ai_engine 直连" type = "forbidden" source_modules = ["windup_app.web", "windup_app.worker"] forbidden_modules = ["windup_ai_engine"] + +# ── pytest 配置 ───────────────────────────────────────────────────── +[tool.pytest.ini_options] +addopts = "--cov=packages --cov-report=term-missing" 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_auth_middleware.py b/backend/tests/test_auth_middleware.py new file mode 100644 index 00000000..d76cca08 --- /dev/null +++ b/backend/tests/test_auth_middleware.py @@ -0,0 +1,96 @@ +"""AuthMiddleware 鉴权测试。 + +覆盖 token 过期 / 无效 / 缺失等场景,确保返回 HTTP 200 + 业务码 401。 +""" + +from datetime import datetime, timezone + +import jwt + +from windup_app.server.user.service import ( + JWT_ALGORITHM, + JWT_SECRET, +) + + +def _make_expired_token(user_id: int = 1, email: str = "test@example.com") -> str: + """生成一个已过期的 access_token(exp 设在1小时之前)。""" + now = datetime.now(timezone.utc) + payload = { + "sub": str(user_id), + "email": email, + "type": "access", + "iat": now, + "exp": now.timestamp() - 3600, # 1小时前过期 + } + return jwt.encode(payload, JWT_SECRET, algorithm=JWT_ALGORITHM) + + +def _make_invalid_signature_token(user_id: int = 1, email: str = "test@example.com") -> str: + """生成一个签名无效的 token(用错误的密钥签发)。""" + now = datetime.now(timezone.utc) + payload = { + "sub": str(user_id), + "email": email, + "type": "access", + "iat": now, + "exp": now.timestamp() + 900, + } + return jwt.encode(payload, "wrong-secret-key", algorithm=JWT_ALGORITHM) + + +class TestAuthMiddlewareTokenValidation: + """token 验证相关用例。""" + + def test_expired_token_returns_401(self, client): + """已过期的 access token 应返回 HTTP 200 + code=401。""" + token = _make_expired_token() + resp = client.get( + "/media/list", + headers={"Authorization": f"Bearer {token}"}, + ) + assert resp.status_code == 200 + body = resp.json() + assert body["code"] == 401 + assert "已过期" in body["message"] + + def test_invalid_signature_token_returns_401(self, client): + """签名无效的 token 应返回 HTTP 200 + code=401。""" + token = _make_invalid_signature_token() + resp = client.get( + "/media/list", + headers={"Authorization": f"Bearer {token}"}, + ) + assert resp.status_code == 200 + body = resp.json() + assert body["code"] == 401 + assert "无效" in body["message"] + + def test_missing_auth_header_returns_401(self, client): + """未携带 Authorization header 应返回 HTTP 200 + code=401。""" + resp = client.get("/media/list") + assert resp.status_code == 200 + body = resp.json() + assert body["code"] == 401 + + def test_malformed_auth_header_returns_401(self, client): + """Authorization header 格式错误(无 Bearer 前缀)应返回 401。""" + resp = client.get( + "/media/list", + headers={"Authorization": "Token abc123"}, + ) + assert resp.status_code == 200 + body = resp.json() + assert body["code"] == 401 + + def test_valid_token_passes_through(self, auth_client): + """有效的 access token 应正常通过鉴权。""" + resp = auth_client.get("/media/list") + # 不是 401 就算通过(可能是业务层的其他状态码) + assert resp.json().get("code") != 401 + + def test_whitelist_path_no_auth(self, client): + """白名单路径不需要鉴权。""" + resp = client.get("/health") + # 白名单路径不应返回 401 + assert resp.json().get("code") != 401 diff --git a/backend/tests/test_project_api.py b/backend/tests/test_project_api.py new file mode 100644 index 00000000..5ee2aa39 --- /dev/null +++ b/backend/tests/test_project_api.py @@ -0,0 +1,125 @@ +"""项目 CRUD API 集成测试。 + +通过 ``TestClient`` 打全链路:请求 -> 路由 -> service -> SQLite -> 统一响应。 +验证统一响应契约(HTTP 恒 200、code 在 body、``ListResponse`` 分页字段、 +``timestamp`` 默认省略)与 400/404 业务码路径。 +""" + +import pytest + +pytestmark = pytest.mark.skip(reason="project router 未实现,待后续补全") + + +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..e260609b --- /dev/null +++ b/backend/tests/test_user_service.py @@ -0,0 +1,502 @@ +"""``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, + ResetPasswordInput, + UpdateNicknameInput, + 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 = None # 未锁定 + login_input = LoginByPasswordInput(email="login@example.com", password="pass123") + 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 = None # 未锁定 + service._redis.incr.return_value = 1 + login_input = LoginByPasswordInput(email="login@example.com", password="wrong") + + 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 = None # 未锁定 + service._redis.incr.return_value = 1 + login_input = LoginByPasswordInput(email="no@example.com", password="pass123") + + 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 = None # 未锁定 + login_input = LoginByPasswordInput(email="banned@example.com", password="pass123") + + 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 = None + login_input = LoginByPasswordInput(email="change@example.com", password="newpass123") + 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) + + +# -- 昵称修改测试 -------------------------------------------------------- + + +def test_update_nickname(db_session, service, mock_email): + """修改昵称后立即生效。""" + service._redis.get.return_value = "123456" + register_input = RegisterInput(email="nick@example.com", password="pass1234", code="123456", nickname="旧昵称") + result = service.register_by_email_with_session(db_session, register_input) + + update_input = UpdateNicknameInput(nickname="新昵称") + user_view = service.update_nickname_with_session(db_session, result.user.id, update_input) + + assert user_view.nickname == "新昵称" + assert user_view.id == result.user.id + + +def test_update_nickname_max_length(db_session, service, mock_email): + """昵称长度上限 50。""" + service._redis.get.return_value = "123456" + register_input = RegisterInput(email="nick2@example.com", password="pass1234", code="123456") + result = service.register_by_email_with_session(db_session, register_input) + + long_nickname = "a" * 50 + update_input = UpdateNicknameInput(nickname=long_nickname) + user_view = service.update_nickname_with_session(db_session, result.user.id, update_input) + + assert user_view.nickname == long_nickname + + +def test_update_nickname_user_not_found(db_session, service): + """用户不存在时抛异常。""" + update_input = UpdateNicknameInput(nickname="test") + + with pytest.raises(BizException, match="用户不存在"): + service.update_nickname_with_session(db_session, 999999, update_input) + + +# -- 重置密码测试 -------------------------------------------------------- + + +def test_reset_password(db_session, service, mock_email): + """邮箱+验证码重置密码后,新密码可登录。""" + # 先注册 + service._redis.get.return_value = "123456" + register_input = RegisterInput(email="reset@example.com", password="oldpass123", code="123456") + service.register_by_email_with_session(db_session, register_input) + + # 重置密码(验证码 purpose 为 reset_password) + service._redis.get.return_value = "654321" + reset_input = ResetPasswordInput(email="reset@example.com", code="654321", new_password="newpass123") + service.reset_password_with_session(db_session, reset_input) + + # 用新密码登录 + service._redis.get.return_value = None + login_input = LoginByPasswordInput(email="reset@example.com", password="newpass123") + login_result = service.login_by_password_with_session(db_session, login_input) + + assert login_result.user.email == "reset@example.com" + + +def test_reset_password_wrong_code(db_session, service, mock_email): + """验证码错误时拒绝重置。""" + service._redis.get.return_value = "123456" + register_input = RegisterInput(email="reset2@example.com", password="oldpass123", code="123456") + service.register_by_email_with_session(db_session, register_input) + + # 验证码错误 + service._redis.get.return_value = None # 验证码过期 + reset_input = ResetPasswordInput(email="reset2@example.com", code="000000", new_password="newpass123") + + with pytest.raises(BizException, match="验证码已过期"): + service.reset_password_with_session(db_session, reset_input) + + +def test_reset_password_user_not_found(db_session, service): + """用户不存在时拒绝重置。""" + service._redis.get.return_value = "654321" + reset_input = ResetPasswordInput(email="noexist@example.com", code="654321", new_password="newpass123") + + with pytest.raises(BizException, match="用户不存在"): + service.reset_password_with_session(db_session, reset_input) + + +# -- 登录限流测试 -------------------------------------------------------- + + +def test_login_account_locked(db_session, service, mock_email): + """账号被锁定后拒绝登录(即使密码正确)。""" + # 先注册 + service._redis.get.return_value = "123456" + register_input = RegisterInput(email="lock@example.com", password="pass123", code="123456") + service.register_by_email_with_session(db_session, register_input) + + # 模拟账号锁定 + service._redis.get.return_value = "1" # lock key 存在 + login_input = LoginByPasswordInput(email="lock@example.com", password="pass123") + + with pytest.raises(BizException, match="邮箱或密码错误"): + service.login_by_password_with_session(db_session, login_input) + + +def test_login_failure_records_count(service, mock_redis): + """错误密码时调用 redis.incr 记录失败次数。""" + mock_redis.get.return_value = None # 未锁定 + mock_redis.incr.return_value = 1 + + service._record_login_failure("test@example.com") + + mock_redis.incr.assert_called_once() + mock_redis.expire.assert_called_once() + + +def test_login_failure_triggers_lock(service, mock_redis): + """连续失败达到上限时触发锁定。""" + mock_redis.incr.return_value = 5 # 第5次失败 + + service._record_login_failure("test@example.com") + + # 验证设置了 lock key + mock_redis.setex.assert_called_once() + args = mock_redis.setex.call_args[0] + assert "login:lock:" in args[0] + + +def test_login_success_clears_failures(service, mock_redis): + """登录成功后清除失败计数和锁定。""" + service._clear_login_failures("test@example.com") + + # 验证删除了 fail key 和 lock key + assert mock_redis.delete.call_count == 2 diff --git a/backend/uv.lock b/backend/uv.lock index cf241f39..bfcbe78f 100644 --- a/backend/uv.lock +++ b/backend/uv.lock @@ -14,6 +14,7 @@ members = [ dev = [ { name = "import-linter", specifier = ">=2.0" }, { name = "pytest", specifier = ">=8.0" }, + { name = "pytest-cov", specifier = ">=5.0" }, { name = "ruff", specifier = ">=0.6" }, ] @@ -48,6 +49,72 @@ wheels = [ { url = "https://mirrors.aliyun.com/pypi/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl", hash = "sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494" }, ] +[[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 +206,117 @@ 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 = "coverage" +version = "7.15.4" +source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/be/c3/4f2195f512fb172aa425a8803a874b2baa9ba7f80ff7b6080998761fc701/coverage-7.15.4.tar.gz", hash = "sha256:0548198fff07ccf4faf469520bce1c2eceb1ce3e62891921138dec10907f9d00" } +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/1d/48/bc8d4ba7b37551a767bd863f15b3f80182b271c2f55975356f5f7dbe94c2/coverage-7.15.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d4fedd1f7f428f9fe83b1ead5e7cc87a43427be31aadafbac3ac0636dc7abb22" }, + { url = "https://mirrors.aliyun.com/pypi/packages/20/dd/88d6f83f1fffc974a3691a34a97951c5b12df7512a6782c5963883cbc058/coverage-7.15.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:37e2f0cdf58e2e1fed4e4d5a8f8786ae2f7eb80b478016876667dc4a01d60a97" }, + { url = "https://mirrors.aliyun.com/pypi/packages/bd/5c/54ee0d4748585bb0acab9891cd8d92f2d3593165b4e59fc9de113bfb3140/coverage-7.15.4-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:fb55d0e70bb15f2e81477613627286581414693d74ac7963c93a790dd453ca9d" }, + { url = "https://mirrors.aliyun.com/pypi/packages/8c/3f/f0642a372f494bd0d7dad3b497083b910194a5f1c88be2c94fef707c3b59/coverage-7.15.4-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:899b9da30f3c6c336566e3707495bb23e8302d39d862f01fa78c48b99b9437e2" }, + { url = "https://mirrors.aliyun.com/pypi/packages/71/17/8b46d0ed68251016002ec972c8fc0119961a765d0984cafb8bf317c43758/coverage-7.15.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d15715e8c46552827e5e4f30a35575a2dbcad14454cf3284c54483946bd16931" }, + { url = "https://mirrors.aliyun.com/pypi/packages/30/b8/8498a0e72d0adbe15477dd07463d2b3bb2c9f6a4815e8589e50939e2c3ae/coverage-7.15.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:002a438859f7b430bc99afeaf01a6d187dad1d0dc907b64cdeffc632a5db8fd8" }, + { url = "https://mirrors.aliyun.com/pypi/packages/41/e1/7dce19c3bdb1e3dd63e769508216500edad81bd5f69a26d724e32aceaf78/coverage-7.15.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e4193a04b518f7968f3099755f5509ee7cccc6dc2b92a6b14841934d22e222c9" }, + { url = "https://mirrors.aliyun.com/pypi/packages/dd/b1/e1494703c675a2561723cd9b89f45c9168782c31280c611b1f767851e57c/coverage-7.15.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e98dcc55d572b38e69d117da7e8e8efb8500f1f5eaf81ecd460a63220790b839" }, + { url = "https://mirrors.aliyun.com/pypi/packages/73/76/a5629d270fb638a43a4b10466f51e2f49d532c1aa4da2913cbbb150bbe0a/coverage-7.15.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:af6c538498ce66c10d3fd541c2a8d5b03da5850355add34e6cba564210cb9e72" }, + { url = "https://mirrors.aliyun.com/pypi/packages/ff/4f/9c44447218435d5766b911534f9d798144a5560f85e9a54ebe5f3f5d19f9/coverage-7.15.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1d10025d96ea89fc2f73714dbc4cbd433fe012c1ac9e23f895d7728b238b6e52" }, + { url = "https://mirrors.aliyun.com/pypi/packages/de/36/c1e127616fb3fa18a9ff71e76c417f2fd7424332a4870015ac224ef4c039/coverage-7.15.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d802e1947603162ded419bff83ac7489820355d2b856dfb09206574e3a37ac0c" }, + { url = "https://mirrors.aliyun.com/pypi/packages/e9/b9/fdb92c8ae7a8bb9b850cc253b7b3b9c8526f68130002048b5671cd510d09/coverage-7.15.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c2de40895718f91951b86712b4c5b694acaf9a0a49be13874896f599a1eed3f4" }, + { url = "https://mirrors.aliyun.com/pypi/packages/6f/c0/a7d51b2587c7bdb76e71b0896d2565bf7d60436b5122fc83e511adb1f7cd/coverage-7.15.4-cp312-cp312-win32.whl", hash = "sha256:5c3431b2161279b7db5c2a1aa58ae02e5cb8c3c42d93a5094be3f5537bd5b11b" }, + { url = "https://mirrors.aliyun.com/pypi/packages/49/b9/5c5f80cc55f5acaaca6dee677626bfcec8c87204a7809b438b08e84f4571/coverage-7.15.4-cp312-cp312-win_amd64.whl", hash = "sha256:6befeab5fb2b51c958ca4ac6c5d141a1e8240f4f76e46350f1911963deda49cd" }, + { url = "https://mirrors.aliyun.com/pypi/packages/47/e4/2a4561f89ff6bf7c925c287d0f2cce8bdf139c3a33735c87e3203401cf94/coverage-7.15.4-cp312-cp312-win_arm64.whl", hash = "sha256:67bc345491ab55b837277d76f5775d057e8c7f1ac44d890d8c2c82adde258c6f" }, + { url = "https://mirrors.aliyun.com/pypi/packages/f1/84/651a9310859673aaa3b3203f1aa1641ca60fcf2494683e1c9474c7172780/coverage-7.15.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c705b28feb2775dc82a25f1d473a370bc37ff93f5177f4e29ce2425f560f6921" }, + { url = "https://mirrors.aliyun.com/pypi/packages/82/f9/4dcf700137e8af550670f4d74d1b63828ce93e1e2b05e5f10710eb2ea987/coverage-7.15.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3ff205ab5e3ecc670f6a4dd19d9cbf12ede53dd41cfc1e15716ec961ea6d314e" }, + { url = "https://mirrors.aliyun.com/pypi/packages/07/4a/612ff1e780b3fbfd637486f542f84adc5503873d8b5d279dec1ffeef9414/coverage-7.15.4-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5172326e861a38b48b48befca15e0f477a26b283337a33a739c8fed229934e36" }, + { url = "https://mirrors.aliyun.com/pypi/packages/b0/04/d1cff1c2ead4708a6a79c01d3736b6a25bd38a36678398f72a8dd33dfad9/coverage-7.15.4-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:12b59c90084e3234fb11184886bf4a40f4f16a8c8f867be2e087b81f8e8868d4" }, + { url = "https://mirrors.aliyun.com/pypi/packages/b9/80/d34e13fb4b293cbdb9665838cf5522077b8ad14ef947550631a4bced36a5/coverage-7.15.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:349062d66f00b40fa2c1c222438bad25fabf755631b5d82937fe985c8008615c" }, + { url = "https://mirrors.aliyun.com/pypi/packages/0f/e7/2c5fe7636fdb0732fe0f09f308a5b066864078b7fc61f6678e8478554f2e/coverage-7.15.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4256ced708e598e05209bc1a8ab4074e04a51dba4c62fb45926a229af675ace7" }, + { url = "https://mirrors.aliyun.com/pypi/packages/92/28/9689f0858dfff59c2ea688938ab9fa2925631235df67126a42b6c5c70ae1/coverage-7.15.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d80f974b20782d9612c8b4c9beeca867074c7cf4079d1419843fa25a26428b25" }, + { url = "https://mirrors.aliyun.com/pypi/packages/f9/e2/785077c230c157243eb5aa9a26c3be260ecd02001bead54a3cada3df8e03/coverage-7.15.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2e179f19bfe1d31f8eeeaa12990194d761c4f62f0759661000bca6cd8729f40b" }, + { url = "https://mirrors.aliyun.com/pypi/packages/d4/90/e20371b17b40f912f21305c2db2f30efa3de306f7320fc916804872c85a4/coverage-7.15.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8bc16bb47b7679670eceff71d78bfb7d6e5b143f6c2cd117487ec7c75e0d4b78" }, + { url = "https://mirrors.aliyun.com/pypi/packages/05/49/25371987ee459a5f67c0427fb75c74f9358e65f2c71fe75bf41c1b6c5fcb/coverage-7.15.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:1cd685005cd2c4200adfc14cf39a603b9320efab3f18a8f7f156d20c9cc3345f" }, + { url = "https://mirrors.aliyun.com/pypi/packages/30/6e/32e67467f6154bf4f1c4f63b05acc5097cba4237d45bbeeea446b52e8ac1/coverage-7.15.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:337399ad2c93b3acd2a937627dae8b3e86b66707cd3d3e856347999aadf1ef8d" }, + { url = "https://mirrors.aliyun.com/pypi/packages/03/c1/8b24192e89286399765155251f99ee9f070a9d637109018ac23d99b99f6f/coverage-7.15.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:96e257121228ec5cd2bb919276e94ac11074471bc37d68dbae0e8308cce15fff" }, + { url = "https://mirrors.aliyun.com/pypi/packages/16/6f/8b41ebdf67c87854e17c035336a90f1cfbad0c14c2a584301be6ff148718/coverage-7.15.4-cp313-cp313-win32.whl", hash = "sha256:c65a9e0dfc6143491879da4e13b5e30f8be192055de508d737fb14601edbd22c" }, + { url = "https://mirrors.aliyun.com/pypi/packages/e0/e2/2946c7f0b42b152ecb21ff1bdad72e3d301e790c0c487e4a86e8c9f69347/coverage-7.15.4-cp313-cp313-win_amd64.whl", hash = "sha256:2ff8f5e9b8f7a94f0c11c45631eee103dbcb7d63274edd12c56efe1be690b3b4" }, + { url = "https://mirrors.aliyun.com/pypi/packages/9e/83/3f4a69957f48ae7a0aba76c34743f88963d607b19e03f3f8e66f91cae0f9/coverage-7.15.4-cp313-cp313-win_arm64.whl", hash = "sha256:6e0a8a5083b096487d6cfced94cdd514d8f5db6f113610fb36c0620edb1028cf" }, + { url = "https://mirrors.aliyun.com/pypi/packages/ea/ac/748cf29eeb2d6be34a3176ce26a4f49e38085ee08e8935f05f6f26ed7e0f/coverage-7.15.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:770e9325ab5ea6d56f77e59b29ecfe0ac20b57a82a601876f90494a4dda0386f" }, + { url = "https://mirrors.aliyun.com/pypi/packages/0b/02/1abbf5c984677b0aa439cdacaccbf38d248939d8ef8fe1cc7a50d73edb77/coverage-7.15.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d12b33a3a50a1676b7784dc8d00a0c6d66a9f2add4b85a041c19b6a7e53ef23c" }, + { url = "https://mirrors.aliyun.com/pypi/packages/eb/e1/ff8f9f53d9fcf586125b55d0b1f04ec1c14955fee41e83d5814bee141bb5/coverage-7.15.4-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5669c8378ebde86f5def7a25d29586631b58acc27ffde04399f678f3dfc6e082" }, + { url = "https://mirrors.aliyun.com/pypi/packages/a1/26/595759762e514e81be1d7d01ed03444303bcd152226a6529998d253f9201/coverage-7.15.4-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ff97a14362eef486483ed44042ca2027ea257df6ff768e62358ee0c9776925ac" }, + { url = "https://mirrors.aliyun.com/pypi/packages/24/68/b79aabac54d482be23b5fcdd4f4662bff24a78edc4ee29201726929936d5/coverage-7.15.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a325e815318638aed1655d9c06e6d7c2d3d46c09231ce988070428a8762d734" }, + { url = "https://mirrors.aliyun.com/pypi/packages/09/0f/bf7f297885a5bf6fd71e5782404e0ff059ca09e8711ceb3a08544abde45a/coverage-7.15.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:474223409d88eb20d2d6a0d37ea60e8647a65a90cc008dc1f0410af5f64f1e0d" }, + { url = "https://mirrors.aliyun.com/pypi/packages/fd/f1/296744e854ff8368542343457414380465e9ceefb9192342feb9d3bc461d/coverage-7.15.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7f2f62ae3cd189dd2e13aece758c57b3eecbd27be070dbd4cbd10936049e5dbf" }, + { url = "https://mirrors.aliyun.com/pypi/packages/55/b0/bbdb2e9057493e66220a2e149ca2d301ba0e3a58a83bd6b90de9826d16f3/coverage-7.15.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:39ece820e29e0a2ba34b3ecb3be83c27e997eed8926f2ba6fe7ce7a0bda5843b" }, + { url = "https://mirrors.aliyun.com/pypi/packages/96/e4/38015b2b6d21258713bd17e76b59d033b191efb5703589cffd037dfbca20/coverage-7.15.4-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:f21b56dcace11dfe013014201f577dcd592b2a9b72182d930361b47cf6f73f25" }, + { url = "https://mirrors.aliyun.com/pypi/packages/0b/64/0d515c1e60ee6fbfd1a0e79c07cd87d388a233b7adc37758735677203808/coverage-7.15.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:93a3a0b662abcc10c73a47cbc72cd60f63618d6989fb2d1286e50eacd974f303" }, + { url = "https://mirrors.aliyun.com/pypi/packages/91/71/04d9e7a3642146c6351338aef4ef85ab11dbbb54744c13245caba1aad1c0/coverage-7.15.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:141fae2cabf5569b782c10afc4c850ce10f618c13f8db54765cba99cc839da1f" }, + { url = "https://mirrors.aliyun.com/pypi/packages/b4/a7/6c28b74c81ebff66987b0e2522ba5cffa3e90b0c33cb6a2eb264d4ee8cf1/coverage-7.15.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:81294c7e6ab30c5f74c0353b11b2fd6320e72d9bee6ac73b357caa8b916323a5" }, + { url = "https://mirrors.aliyun.com/pypi/packages/52/af/bc19996a7014b98d7bbb0f0939453c67074af65784a3aa16a789a07381fa/coverage-7.15.4-cp314-cp314-win32.whl", hash = "sha256:7bbd7d6418e0dab31a206af5203bd43ae36edb8e7fba1940b055d3e9249290d7" }, + { url = "https://mirrors.aliyun.com/pypi/packages/ee/90/219484e476d6e101ba0a444852579e05f5b75c37c611a42ed1190f73ef62/coverage-7.15.4-cp314-cp314-win_amd64.whl", hash = "sha256:f0204ed122758782970526057093f448051a39db9d810d4e344bb87a3546f425" }, + { url = "https://mirrors.aliyun.com/pypi/packages/b7/66/fa77daf4e383e5f776dac62c2409b6af81910ae6fe326bd5170dba74cc63/coverage-7.15.4-cp314-cp314-win_arm64.whl", hash = "sha256:9e71e7bc71c686a123347ae47a0de33a175e797a85bb57b791492adf4eec8ed8" }, + { url = "https://mirrors.aliyun.com/pypi/packages/58/5b/f03bf0ce362bbf3f785fa5219620d00778d4ac6fc9e407734828e9c672f6/coverage-7.15.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7c922735321eef3f87c280a3d39afff6b646723a2880b862cda4ac7a093b8aa8" }, + { url = "https://mirrors.aliyun.com/pypi/packages/0f/76/e77d0ae22501831cc9f92193e8a957a5caa1dd177f90a6d1d9b106242d92/coverage-7.15.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f41c17c4668a655ce96d090d8d5ffdc24ef64b5a02f9753884d08483e8a4a41a" }, + { url = "https://mirrors.aliyun.com/pypi/packages/82/1a/b1f089da8d38ac612fa2dd6dc7f4a1a7657d12f3e261d2996edd3a838d0b/coverage-7.15.4-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:46822e9b6ff1c6a72b518c162c44a8f45a61a1d609c51084bf5b16c023c5037b" }, + { url = "https://mirrors.aliyun.com/pypi/packages/bf/31/e66d98d6e9c7fcc88470f1e234eaf6b1950dc0dfbf797f7282c1c861da24/coverage-7.15.4-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3d6f4955b73b5445271379a59e3792b0d978f42d4a01e0cf7a67d9c33a3bb0a5" }, + { url = "https://mirrors.aliyun.com/pypi/packages/59/a1/ae94eb2c541add426378408379f233591e069040b1e2cdb33df9498a0682/coverage-7.15.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3fc9e047706fb4a9abb54f719d3aa643e80e5bb3818182c40aee01ac0f0247ba" }, + { url = "https://mirrors.aliyun.com/pypi/packages/9c/c7/88a10694a1c6a213569766aba9f25847b28155d4ac731b13226db216356d/coverage-7.15.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:05e491d4f3165d62d4f5c8fd48dfeabf2ae8f42cbbd484319af33ea851b78982" }, + { url = "https://mirrors.aliyun.com/pypi/packages/b3/34/d8b8232e5e55169933b59aabcef2fedfa4b9d8897361bb80fcbda146505f/coverage-7.15.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:226c66e80ec0598d3b9b4874123df167ccca342aca8714f77cac6829688ee09c" }, + { url = "https://mirrors.aliyun.com/pypi/packages/7e/35/58b009dbf8c471c7224716478b9fed4a7e1af15320e1ed41660978504663/coverage-7.15.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ac41cc14bebda0dbfb0628036b7f75706935c95bcc07fefe9a0f93614aa60a57" }, + { url = "https://mirrors.aliyun.com/pypi/packages/62/aa/57fbda1b42c892968273c56b6ee9dc0f1310850859230a507bc7873b1f65/coverage-7.15.4-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:8af623e5cd92080acddd02b38f2f406a2c3a0893c38950b211890361448fbf26" }, + { url = "https://mirrors.aliyun.com/pypi/packages/98/8a/360e6e7f24d477b7e889703af0afa878d15b6d4d8d2a822b2835c169a879/coverage-7.15.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:07545711d4f0f32852a18f18ad11f76f0109909d09e78b9008b4cfc67e829429" }, + { url = "https://mirrors.aliyun.com/pypi/packages/4e/89/6f701261aee21b6b5fa8f7872229406dc917e125069448292223bf213606/coverage-7.15.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:a0865421cfdc53654b342d515e5a233187590882d20b95752150e53f65460017" }, + { url = "https://mirrors.aliyun.com/pypi/packages/3f/0f/6f04036edc260ed425af83e834f627fad48941ce97b50bfe6edd8b6fa623/coverage-7.15.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:460115e32ee40566476db5048f9bec1e842c127ad8e6f8be745aad3ac9cbc839" }, + { url = "https://mirrors.aliyun.com/pypi/packages/c4/ce/d19b5d4d5c49a7bfb925fd74310fee7d28bc99520ac3367ccbc54e662518/coverage-7.15.4-cp314-cp314t-win32.whl", hash = "sha256:cbde877ef9dd7baf272b9bfef2b8a25edd45d9170fc326951dd20eb480335e85" }, + { url = "https://mirrors.aliyun.com/pypi/packages/26/bb/7aa1b3b173faee0679037ca950bbbe1247273656697994d8d13f80f8d4b4/coverage-7.15.4-cp314-cp314t-win_amd64.whl", hash = "sha256:3da9e92d1c551fd7563833e9ade686efb0c4b7363ab7681a94283958c950bf5e" }, + { url = "https://mirrors.aliyun.com/pypi/packages/81/1c/4ea9e47426d80038d9222db3c4534cb6021a74b237d3ff97ffd33b6600dd/coverage-7.15.4-cp314-cp314t-win_arm64.whl", hash = "sha256:3a54f5a0d85050c73a38f6793090ee83974531e67fe5e57a1da9bee11398aa5e" }, + { url = "https://mirrors.aliyun.com/pypi/packages/2b/c4/dc5d2ac8f9142e7ec7de66e7bf0591db29d78955a040bd915870d9c0e657/coverage-7.15.4-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:2c9872e4d9dc5d3cf616bf4b382f5a00359305a5be666a3dd0b5cdb4e49597f9" }, + { url = "https://mirrors.aliyun.com/pypi/packages/70/39/33e63df81fe2ee100897451841c821467635923e58e37c6bd4b46dd8106c/coverage-7.15.4-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:e101dbb4b9b72f0cddd8cdc8c9c5b47f456766f5e0ac82dbfb75e5c55409b78a" }, + { url = "https://mirrors.aliyun.com/pypi/packages/99/1f/ef3ffb5557febc75a0d97aa459d0266d7d741110265121cc6d8539343d44/coverage-7.15.4-cp315-cp315-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7d1abebdb047729e852b9c77a00497dfbeb11eb3a117e037d7dbc3ac8e5f5c54" }, + { url = "https://mirrors.aliyun.com/pypi/packages/6f/f5/1f0f6f77698c3601ca0ae7431e34b24c62ca2f06fecb23b73ed1f651d2be/coverage-7.15.4-cp315-cp315-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d28a4a899354d0ea6214cc59b4fa19eefbce1b9ff1688ab579acf49e894bd3fb" }, + { url = "https://mirrors.aliyun.com/pypi/packages/03/7a/2ed9bed79925f4367c83c77f66a89e5ca7229c288d2d19ad5f36d1ca0070/coverage-7.15.4-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ffb3c2aacea411cc7e1d27712490c11108e2de1d39019ae32915493a59a8b9ed" }, + { url = "https://mirrors.aliyun.com/pypi/packages/45/8c/fa34044f71b7cc4ecb6da9c2408770959b0591fa9b5fb6fb6bca38f94298/coverage-7.15.4-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a9447978a92f405d301123cfd39ff49895490efb769a758fe2734c7f631bf8ce" }, + { url = "https://mirrors.aliyun.com/pypi/packages/4f/54/d5727ce36b4524a7394ab9f5f1df378e1f23affcdab01037dc8655185cc7/coverage-7.15.4-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:050467a7983b8e2fe7dd41a78bb30c3e7f8c0b8cafda14b1c46f8b5e3cf2dd3c" }, + { url = "https://mirrors.aliyun.com/pypi/packages/dc/e6/6e3783e576719590194bdffb6dd6d85490801785b7c331e35a245d8cb8b5/coverage-7.15.4-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:d003b7a5708ddad5c206c79607a6b92abb6fc13c57d99d8a4468cc03a2941ced" }, + { url = "https://mirrors.aliyun.com/pypi/packages/dc/f2/bacdbde18b69ed2de424fcf64d9fb0a4913753d4f0eca8bae9daad69f4bd/coverage-7.15.4-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:c38efe30fd74e5c19e9433f11fb1f5dc9c6522770971b7c6145bbaa413dc8800" }, + { url = "https://mirrors.aliyun.com/pypi/packages/6c/a3/1fb927196e3477c1b48831169ab58ba08f451ba87ae311ff1de68b26a616/coverage-7.15.4-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:1f4f826d70f772ab8b0c052329580d7fe8b8abd191e4ce0c8f81aec6614665d3" }, + { url = "https://mirrors.aliyun.com/pypi/packages/41/58/30d4c149c69053de0edfe325614c1d28d508f62b1783e0e4a234d2e49136/coverage-7.15.4-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:4a4bf917c9953f57c957be31c1cd504e3bd2f34d4a352b9d391a3025336f6768" }, + { url = "https://mirrors.aliyun.com/pypi/packages/89/e4/77f639371b918aad30dda4051f95404b43578f7f2e2f87ba73e02ed1ff37/coverage-7.15.4-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:1c9bf40ebef178a45192c75c4964760bb261b0e6ad725da5fc4c93f674f19753" }, + { url = "https://mirrors.aliyun.com/pypi/packages/5c/62/13be29b3ddab35f14c87967a4820a05106d2a3eccb4fa4ff550bf30b75e0/coverage-7.15.4-cp315-cp315-win32.whl", hash = "sha256:43619d04c3671792d2c4706ae8bf45e265dc87bbd4078189ef8b847ea1e74be2" }, + { url = "https://mirrors.aliyun.com/pypi/packages/a1/70/af0c6be0f964af6954f6b74bc109b0dbca02824696d2520fb17fe1ab06e3/coverage-7.15.4-cp315-cp315-win_amd64.whl", hash = "sha256:be619439dbcd31a2eab10b32de9fff62c26ed4bab69dc32b8363fdaaa0882809" }, + { url = "https://mirrors.aliyun.com/pypi/packages/4f/2d/f3bd3aab899fc9efc18b53133ee68f5f98574ef480649b23e12962226387/coverage-7.15.4-cp315-cp315-win_arm64.whl", hash = "sha256:def597967dafc2e8d97c9097ea453c464e0bb8ed38f193a43070f10dc623bb6d" }, + { url = "https://mirrors.aliyun.com/pypi/packages/f5/ca/f69251cd63eabc6438321aea22148754cce758a26bde07dd490e3fe7cfc5/coverage-7.15.4-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:c7dbc748ac8a1e3e59a2b28bea47675e6e778081dbbf081bde0d75def2fcbe1d" }, + { url = "https://mirrors.aliyun.com/pypi/packages/a7/a7/037b53b2885b0d8447064432491a4d5a1014cd9f97a594d53acd0c04541a/coverage-7.15.4-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:2413074a5ecbb61a01a7888fc72db0ca324d13588c5b38bc0dd8564cdcdfea26" }, + { url = "https://mirrors.aliyun.com/pypi/packages/80/4f/152b8a4779ae90da11bb24f7467df8a59f0be48a5c52acb856325ca48289/coverage-7.15.4-cp315-cp315t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4e6f6f632b7b2f714bf7a1346e8f97b650ee71f3c298aaad42a2ab60f0f07645" }, + { url = "https://mirrors.aliyun.com/pypi/packages/10/2d/84b4b9e0e1dd6528a51920ff7031f35b789382e467a28ec6a5a578cb8812/coverage-7.15.4-cp315-cp315t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8df457da2249d3c75ca2e5e835d59c725abfe92d27fdff6cd99eed85b51d5e9a" }, + { url = "https://mirrors.aliyun.com/pypi/packages/53/fc/ba01cc25299f9f8a2c8b02d3b28c53f3543d9fbfbe4e74fa2760b48f163e/coverage-7.15.4-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:050f66a08805acb5b8a23c6d4a517b1ecf82c08e81ed0e4bd727df065e5c6624" }, + { url = "https://mirrors.aliyun.com/pypi/packages/cf/d0/db2647cbf40b14f8c308f94ff7bf89c06d564e59f396906edf50086ec788/coverage-7.15.4-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1587fb771d1ccceef708fdde1e5af8c7ed24b486b61d13a321acb7d8145390aa" }, + { url = "https://mirrors.aliyun.com/pypi/packages/70/ff/4d2d17924552c458bb4f77dd631f0e3bc92fbbdf2d2d916cd4b33bbfd5b1/coverage-7.15.4-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8b4f1c3a69ca580f3fbd6b2046915f536d7f586874f25c1bb23add2a3c88d50f" }, + { url = "https://mirrors.aliyun.com/pypi/packages/ee/de/dc010c7a3691f396d93bbc26bfcafa1c2a3a351cd520470f15faf5795bd5/coverage-7.15.4-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:ffb58d7eff5b7f6ecc6fa21d6288ab7f968a212cb67d682c269c09b9eba3b66f" }, + { url = "https://mirrors.aliyun.com/pypi/packages/78/ea/dc96a11375e83c045c2f7c61fb6918277cfe9401db7c0f7b1d111a84b2e5/coverage-7.15.4-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:d9df165544774574ee004b953023d1bebada1894a80b1052a43d798b0f676e67" }, + { url = "https://mirrors.aliyun.com/pypi/packages/c8/86/b77131a0f9503ce461cd577076147d7a9040f0c5dda772686f729e2cc9cb/coverage-7.15.4-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:f9de0a24a4079b53e523b5c5e2c5945ec251ab486652659955187cf255a259bc" }, + { url = "https://mirrors.aliyun.com/pypi/packages/24/24/944bc35007862955e7ebf05754e645419dcf5d7526c52735cfa2715e8ebf/coverage-7.15.4-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:150089274bdc9f940628552cb92844e0223c987f1902ab8efe9f45a2ec758d88" }, + { url = "https://mirrors.aliyun.com/pypi/packages/c7/cc/a3bb9f93e7e740659163e2ea584f8196ddcd2c456a5dbe15f6c50105fec1/coverage-7.15.4-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:a58a94fed5da6997d258e8f7668c1e195fbd04a691d781b7558f1e468f9e68bc" }, + { url = "https://mirrors.aliyun.com/pypi/packages/49/dd/e0e40f3560d878d888c580698ff5ad1179f5e1c3ac949684ef66b41a3817/coverage-7.15.4-cp315-cp315t-win32.whl", hash = "sha256:ebd5a6d8466ff30836572f3ba2cae8a5e8f85029b1c6d5e2ed338dc472a5166a" }, + { url = "https://mirrors.aliyun.com/pypi/packages/c6/7e/37732ea80eebc30e976e4cdab15c190bc42d96959a42e38ddf6f8c60468f/coverage-7.15.4-cp315-cp315t-win_amd64.whl", hash = "sha256:288bde2a2d7ab6b6c2d7252fcde8b524387f2d970bdba9658fc6f8bbcaef0f9b" }, + { url = "https://mirrors.aliyun.com/pypi/packages/c6/08/1e00f7923eaaba45fb3d51dd794125fc766304b1df264f3a9c6557bfb30e/coverage-7.15.4-cp315-cp315t-win_arm64.whl", hash = "sha256:68be5e1de60ff13c9095bbec0e5a7fa45b33b101752215b91345ea1f61c4a278" }, + { url = "https://mirrors.aliyun.com/pypi/packages/b4/d9/e70c286c979378f061d8266e279b686ab0b0b688e1fe0af864684f23a77d/coverage-7.15.4-py3-none-any.whl", hash = "sha256:964730a1e9de9c0cf11be6a1a3c79ce419c34882842abd256086ba4698705e84" }, +] + [[package]] name = "distro" version = "1.9.0" @@ -148,6 +326,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 +364,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 +564,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" @@ -389,6 +609,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 +718,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 +849,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 +909,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 +1056,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 +1150,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 +1238,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 +1350,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" @@ -975,6 +1375,20 @@ wheels = [ { url = "https://mirrors.aliyun.com/pypi/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c" }, ] +[[package]] +name = "pytest-cov" +version = "7.1.0" +source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } +dependencies = [ + { name = "coverage" }, + { name = "pluggy" }, + { name = "pytest" }, +] +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/b1/51/a849f96e117386044471c8ec2bd6cfebacda285da9525c9106aeb28da671/pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2" } +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678" }, +] + [[package]] name = "python-dotenv" version = "1.2.2" @@ -1039,6 +1453,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 +1589,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 +1703,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 +1724,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" @@ -1482,7 +2089,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 +2101,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 +2127,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 +2147,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" }, ]