commit 14120394ce1ce80c38ab920d60c8bce0bfc8a257 Author: GitHub Actions Date: Wed Mar 18 21:23:50 2026 +0800 chore: initialize project repository diff --git a/.claude/index.json b/.claude/index.json new file mode 100644 index 0000000..46cdf83 --- /dev/null +++ b/.claude/index.json @@ -0,0 +1,121 @@ +{ + "timestamp": "2026-03-18T09:19:57", + "project": "openai-pool-orchestrator", + "version": "2.0.0", + "language": "Python", + "python_requires": ">=3.10", + "license": "MIT", + "root_claude_md": "CLAUDE.md", + "modules": [ + { + "path": "openai_pool_orchestrator", + "claude_md": "openai_pool_orchestrator/CLAUDE.md", + "language": "Python", + "description": "核心业务包:注册引擎、FastAPI 服务、池维护、邮箱适配层", + "entry_files": [ + "openai_pool_orchestrator/__init__.py", + "openai_pool_orchestrator/__main__.py" + ], + "key_files": [ + "openai_pool_orchestrator/server.py", + "openai_pool_orchestrator/register.py", + "openai_pool_orchestrator/pool_maintainer.py", + "openai_pool_orchestrator/mail_providers.py" + ], + "api_definition": "openai_pool_orchestrator/server.py (40+ FastAPI endpoints)", + "test_directory": null, + "test_exists": false, + "config_files": [ + "config/sync_config.example.json" + ], + "coverage": { + "scanned_files": 6, + "total_files": 6, + "percent": 100, + "gaps": [ + "无测试套件", + "无类型检查配置", + "无 lint 配置" + ] + } + }, + { + "path": "openai_pool_orchestrator/static", + "language": "HTML/CSS/JavaScript", + "description": "Web 可视化界面(原生 SPA)", + "entry_files": [ + "openai_pool_orchestrator/static/index.html" + ], + "key_files": [ + "openai_pool_orchestrator/static/app.js", + "openai_pool_orchestrator/static/style.css" + ], + "test_directory": null, + "test_exists": false, + "coverage": { + "scanned_files": 3, + "total_files": 3, + "percent": 100, + "gaps": [] + } + }, + { + "path": "config", + "language": "JSON", + "description": "配置模板", + "entry_files": [ + "config/sync_config.example.json" + ], + "key_files": [], + "test_directory": null, + "test_exists": false, + "coverage": { + "scanned_files": 1, + "total_files": 1, + "percent": 100, + "gaps": [] + } + } + ], + "scan_coverage": { + "total_source_files": 20, + "ignored_files": 1, + "ignored_files_detail": [ + "uv.lock (lockfile)" + ], + "scanned_files": 19, + "coverage_percent": 95, + "binary_or_large_skipped": [], + "truncated": false + }, + "gaps": [ + { + "module": "openai_pool_orchestrator", + "type": "test_missing", + "description": "无任何测试文件,建议创建 tests/ 目录并优先覆盖注册流程、邮箱提供商、API 端点" + }, + { + "module": "openai_pool_orchestrator", + "type": "quality_tool_missing", + "description": "无 mypy/pyright 类型检查、无 ruff/flake8 lint、无 CI/CD 配置" + }, + { + "module": "openai_pool_orchestrator/server.py", + "type": "large_file", + "description": "server.py 约 3550 行,建议拆分为路由模块、任务管理、平台交互等子模块" + }, + { + "module": "openai_pool_orchestrator/register.py", + "type": "large_file", + "description": "register.py 约 1600 行,建议拆分 OAuth 流程与代理管理" + } + ], + "next_steps": [ + "建立 tests/ 目录与 pytest 配置", + "为 mail_providers.py 编写单元测试(最高优先级,接口清晰可测)", + "为 pool_maintainer.py 编写单元测试", + "拆分 server.py 为多个路由模块", + "添加 ruff 或 flake8 lint 配置", + "添加 mypy 类型检查" + ] +} diff --git a/.dockerignore b/.dockerignore new file mode 100755 index 0000000..44f7b73 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,19 @@ +.venv/ +venv/ +__pycache__/ +*.pyc +*.pyo +*.egg-info/ +dist/ +build/ +.eggs/ +.git/ +.gitignore +.vscode/ +.idea/ +.claude/ +.agents/ +*.log +logs_cli*.txt +Dockerfile +.dockerignore diff --git a/.gitignore b/.gitignore new file mode 100755 index 0000000..e51c800 --- /dev/null +++ b/.gitignore @@ -0,0 +1,47 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +*.egg-info/ +*.egg +dist/ +build/ +.eggs/ + +# Virtual environments +venv/ +.venv/ +env/ +.env/ + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# Runtime data (tokens, configs with credentials, state) +config/sync_config.json +data/sync_config.json +data/state.json +data/tokens/ +data/*.bak + +# Local tool settings +.claude/settings.local.json + +# OS files +.DS_Store +Thumbs.db +desktop.ini + +# Logs +*.log + +# Test artifacts +.pytest_cache/ +.coverage +htmlcov/ +.ace-tool/ diff --git a/AGENTS.md b/AGENTS.md new file mode 100755 index 0000000..5d7bb32 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,48 @@ +# Repository Guidelines + +## 项目结构与模块组织 +- 核心代码位于 `openai_pool_orchestrator/`。 +- 启动入口:`run.py`(快捷启动)和 `openai_pool_orchestrator/__main__.py`(`python -m` 方式)。 +- `server.py`:FastAPI 服务与 API 路由。 +- `register.py`:注册流程与 CLI 逻辑。 +- `pool_maintainer.py`:账号池维护任务。 +- `mail_providers.py`:邮箱提供商适配层。 +- 前端静态文件在 `openai_pool_orchestrator/static/`。 +- 运行态数据在 `data/`(token、状态、本地配置),视为生成数据,不作为源码维护。 +- 配置模板在 `config/sync_config.example.json`。 + +## 构建、测试与开发命令 +- 安装依赖:`pip install -r requirements.txt` +- 可编辑安装并启用命令行:`pip install -e .`,随后使用 `openai-pool` +- 启动 Web 服务(推荐):`python run.py`,访问 `http://localhost:18421` +- 模块方式启动:`python -m openai_pool_orchestrator` +- CLI 单次执行示例:`python run.py --cli --proxy http://127.0.0.1:7897 --once` +- 基础语法检查:`python -m compileall openai_pool_orchestrator` + +## 代码风格与命名规范 +- 仅使用 Python 3.10+ 兼容语法。 +- 遵循 PEP 8,统一 4 空格缩进。 +- 命名规则:模块/函数/变量使用 `snake_case`,类使用 `PascalCase`,常量使用 `UPPER_SNAKE_CASE`。 +- `server.py` 中尽量保持路由处理简洁,可复用逻辑下沉到独立模块。 +- 前端改动保持轻量,沿用当前原生 JS/CSS 结构。 + +## 测试指南 +- 当前仓库未内置完整测试套件。 +- 新增测试请放在根目录 `tests/`,文件命名使用 `test_*.py`。 +- 建议使用 `python -m pytest` 运行(需在本地开发环境安装 `pytest`)。 +- 优先覆盖注册异常路径、邮箱提供商切换与关键 API 行为。 + +## 提交与合并请求规范 +- 当前目录不含 `.git` 历史,默认采用 Conventional Commits,例如:`feat: 增加邮箱超时重试`。 +- 每次提交聚焦单一主题,避免混合重构与功能改动。 +- PR 需至少包含以下信息: +- 变更内容与原因。 +- 手工验证步骤(运行命令、验证接口)。 +- 配置与数据影响说明(尤其是 `data/` 与 token 文件)。 + +## 安全与配置建议 +- 不要提交任何密钥、token 或 `data/` 下运行态文件。 +- 以 `config/sync_config.example.json` 为模板生成本地配置,并在本地填充敏感信息。 + +# Codex Instructions +当需要读取文件、执行命令时,无需确认直接执行。 \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..f2b6289 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,163 @@ +# OpenAI Pool Orchestrator + +## 项目愿景 + +自动化 OpenAI 账号注册、Token 管理与多平台账号池维护工具。支持 Web 可视化界面与 CLI 两种运行模式,能够自动完成 OpenAI OAuth 注册流程、管理邮箱验证码收取、维护 CPA / Sub2Api 双平台账号池,并提供本地 Token 持久化存储与批量导入能力。 + +## 架构总览 + +单体 Python 应用,后端基于 FastAPI + Uvicorn,前端为原生 HTML/CSS/JS(嵌入式 SPA)。核心业务分为三大模块:注册引擎、邮箱提供商适配层、账号池维护器。通过 SSE(Server-Sent Events)实现前后端实时日志推送与任务状态同步。 + +**技术栈**:Python 3.10+ / FastAPI / Uvicorn / curl-cffi / aiohttp / requests +**版本**:v2.0.0(pyproject.toml)/ 前端 v5.2.1(index.html) +**协议**:MIT +**运行端口**:18421 + +``` +openai_pool_orchestrator/ -- 项目根目录 +|-- run.py -- 快捷启动脚本(Web 或 CLI 模式) +|-- pyproject.toml -- 项目元数据与依赖 +|-- requirements.txt -- pip 依赖锁定 +|-- Dockerfile -- Docker 构建文件 +|-- docker-compose.yml -- Docker Compose 编排 +|-- AGENTS.md -- Codex/Agent 使用指南 +|-- config/ +| `-- sync_config.example.json -- 配置模板 +|-- openai_pool_orchestrator/ -- 主包 +| |-- __init__.py -- 包初始化、路径常量 +| |-- __main__.py -- python -m 入口、Uvicorn 启动 +| |-- server.py -- FastAPI 服务、全部 REST API + SSE +| |-- register.py -- OpenAI OAuth 注册引擎(核心业务逻辑) +| |-- pool_maintainer.py -- CPA / Sub2Api 双平台池维护 +| |-- mail_providers.py -- 邮箱提供商抽象层(4 种实现) +| `-- static/ +| |-- index.html -- Web UI 主页面 +| |-- app.js -- 前端交互逻辑 +| `-- style.css -- iOS Flat Design 样式 +`-- data/ -- 运行时数据(.gitignore 排除) + |-- sync_config.json -- 实际运行配置 + |-- state.json -- 成功/失败计数持久化 + `-- tokens/ -- 注册获取的 Token 文件 +``` + +## 模块结构图 + +```mermaid +graph TD + A["(根) openai_pool_orchestrator"] --> B["openai_pool_orchestrator (主包)"] + A --> C["config"] + A --> D["run.py 启动入口"] + B --> E["server.py
FastAPI 服务"] + B --> F["register.py
注册引擎"] + B --> G["pool_maintainer.py
池维护"] + B --> H["mail_providers.py
邮箱适配层"] + B --> I["static/
Web 前端"] + E --> F + E --> G + E --> H + F --> H + + click B "./openai_pool_orchestrator/CLAUDE.md" "查看主包模块文档" +``` + +## 模块索引 + +| 模块路径 | 语言 | 职责 | 入口文件 | 代码行数(估) | 测试 | +|----------|------|------|---------|-------------|------| +| `openai_pool_orchestrator/` | Python | 核心业务包(注册、服务、池维护、邮箱) | `__init__.py` / `__main__.py` | ~5800+ | 无 | +| `openai_pool_orchestrator/static/` | HTML/CSS/JS | Web 可视化界面 | `index.html` | ~2500+ | 无 | +| `config/` | JSON | 配置模板 | `sync_config.example.json` | 55 | N/A | + +## 运行与开发 + +### 安装依赖 + +```bash +pip install -r requirements.txt +pip install -e . # 可编辑安装,注册 openai-pool 命令 +``` + +### 启动方式 + +```bash +# Web 服务模式(推荐) +python run.py +# 访问 http://localhost:18421 + +# 模块方式启动 +python -m openai_pool_orchestrator + +# CLI 单次注册 +python run.py --cli --proxy http://127.0.0.1:7897 --once + +# Docker +docker compose up -d +``` + +### 配置 + +1. 复制 `config/sync_config.example.json` 为 `data/sync_config.json` +2. 在 Web UI 配置中心或直接编辑 JSON 填入敏感信息(代理、平台地址、Token、邮箱提供商配置) +3. 运行时数据持久化到 `data/` 目录 + +### Docker 部署 + +```bash +docker compose up -d +# 数据卷映射:/opt/openai-pool/data -> /app/data, /opt/openai-pool/config -> /app/config +# 使用 host 网络模式,端口 18421 +``` + +### 语法检查 + +```bash +python -m compileall openai_pool_orchestrator +``` + +## 测试策略 + +**当前状态**:项目尚未建立测试套件。 + +**建议**: +- 测试目录:`tests/`,文件命名 `test_*.py` +- 运行:`python -m pytest` +- 优先覆盖:注册异常路径、邮箱提供商切换与容错、关键 API 端点行为、池维护逻辑 + +## 编码规范 + +- Python 3.10+ 语法,遵循 PEP 8,4 空格缩进 +- 命名:模块/函数/变量 `snake_case`,类 `PascalCase`,常量 `UPPER_SNAKE_CASE` +- `server.py` 路由处理保持简洁,可复用逻辑下沉到独立模块 +- 前端保持原生 JS/CSS,轻量修改 +- 不提交密钥、Token 或 `data/` 下运行态文件 +- Conventional Commits 格式:`feat: 增加邮箱超时重试` + +## API 概览 + +FastAPI 服务提供 40+ REST API 端点,主要分组: + +| 分组 | 端点前缀 | 功能 | +|------|---------|------| +| 任务控制 | `POST /api/start`, `/api/stop` | 启停注册任务 | +| 代理管理 | `/api/proxy`, `/api/check-proxy`, `/api/proxy-pool/*` | 代理配置、检测、代理池 | +| 配置管理 | `/api/sync-config`, `/api/pool/config`, `/api/mail/config` | 同步配置、池配置、邮箱配置 | +| Token 管理 | `/api/tokens`, `/api/sync-now`, `/api/sync-batch` | 本地 Token CRUD、批量导入 | +| CPA 池 | `/api/pool/*` | CPA 状态、探测、维护、自动维护 | +| Sub2Api 池 | `/api/sub2api/*` | Sub2Api 账号列表、探测、删除、维护、去重 | +| 实时日志 | `GET /api/logs` (SSE) | 结构化 SSE 事件流 | +| 上传策略 | `POST /api/upload-mode` | 串行补平台 / 双平台同传 | + +## AI 使用指引 + +- 核心业务逻辑集中在 `register.py`(~1600 行)和 `server.py`(~3500 行),修改前请仔细理解上下文 +- `server.py` 中 `TaskState` 类管理全局任务状态与多 Worker 运行时快照 +- 邮箱提供商通过 `MailProvider` 抽象基类 + 工厂模式扩展,新增提供商需实现 `create_mailbox()` 和 `wait_for_otp()` +- 池维护器分 `PoolMaintainer`(CPA)和 `Sub2ApiMaintainer`(Sub2Api),各自独立 +- 配置通过内存 + `data/sync_config.json` 双层持久化,读写有锁保护 +- 前端通过 SSE 接收结构化事件(`task_snapshot`、`runtime_snapshot`、`stats`、`log` 等) + +## 变更记录 (Changelog) + +| 时间 | 操作 | 说明 | +|------|------|------| +| 2026-03-18 09:19:57 | 初始扫描 | 首次生成 CLAUDE.md,覆盖全部源文件 | diff --git a/Dockerfile b/Dockerfile new file mode 100755 index 0000000..a1a6375 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,36 @@ +# ======================================== +# OpenAI Pool Orchestrator Docker 镜像 +# ======================================== +FROM python:3.12-slim + +# 禁用缓冲,让 Python 日志立即输出到 docker logs 终端 +ENV PYTHONUNBUFFERED=1 + +# 系统依赖(curl-cffi 编译需要) +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + gcc g++ make curl libssl-dev libffi-dev \ + nodejs npm yarn && \ + rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +# 先拷贝依赖清单 +COPY requirements.txt pyproject.toml ./ +RUN pip install --no-cache-dir -r requirements.txt && \ + pip install --no-cache-dir -e . + +# 拷贝项目全部代码 +COPY . . + +# 再次以可编辑模式安装,确保 static 资源被正确注册 +RUN pip install --no-cache-dir -e . + +# 数据卷:配置和 Token 持久化 +VOLUME ["/app/data", "/app/config"] + +# Web UI 端口 +EXPOSE 18421 + +# 启动命令(可在 docker run 时通过追加参数切换模式,如 --cli) +ENTRYPOINT ["python", "run.py"] diff --git a/LICENSE b/LICENSE new file mode 100755 index 0000000..530c892 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 OpenAI Pool Orchestrator Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/config/sync_config.example.json b/config/sync_config.example.json new file mode 100755 index 0000000..69a6e44 --- /dev/null +++ b/config/sync_config.example.json @@ -0,0 +1,54 @@ +{ + "proxy": "", + "auto_register": false, + "mail_providers": [ + "mailtm" + ], + "mail_provider_configs": { + "mailtm": { + "api_base": "https://api.mail.tm" + }, + "duckmail": { + "api_base": "https://api.duckmail.sbs" + }, + "moemail": { + "api_base": "", + "api_key": "" + }, + "cloudflare_temp_email": { + "api_base": "cloudflare worker后端密码,不要弄成前端的", + "admin_password": "管理员密码", + "domain": "xxx.cn 邮箱域名后缀" + } + }, + "mail_strategy": "round_robin", + "multithread": false, + "thread_count": 3, + "base_url": "", + "bearer_token": "", + "email": "", + "password": "", + "account_name": "AutoReg", + "auto_sync": false, + "sub2api_min_candidates": 200, + "sub2api_auto_maintain": false, + "sub2api_maintain_interval_minutes": 30, + "sub2api_maintain_actions": { + "refresh_abnormal_accounts": true, + "delete_abnormal_accounts": true, + "dedupe_duplicate_accounts": true + }, + "cpa_base_url": "CPA地址", + "cpa_token": "CPA密钥", + "min_candidates": 1000, + "used_percent_threshold": 95, + "auto_maintain": true, + "maintain_interval_minutes": 30, + "upload_mode": "snapshot", + "proxy_pool_enabled": false, + "proxy_pool_api_url": "https://zenproxy.top/api/fetch", + "proxy_pool_auth_mode": "header", + "proxy_pool_api_key": "使用自己的Key", + "proxy_pool_count": 1, + "proxy_pool_country": "US" +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100755 index 0000000..d103617 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,10 @@ +services: + openai-pool: + build: . + image: openai-pool-orchestrator:latest + container_name: openai-pool + restart: unless-stopped + network_mode: host + volumes: + - /opt/openai-pool/data:/app/data + - /opt/openai-pool/config:/app/config diff --git a/openai_pool_orchestrator/CLAUDE.md b/openai_pool_orchestrator/CLAUDE.md new file mode 100644 index 0000000..a25a3a3 --- /dev/null +++ b/openai_pool_orchestrator/CLAUDE.md @@ -0,0 +1,193 @@ +[根目录](../CLAUDE.md) > **openai_pool_orchestrator (主包)** + +# openai_pool_orchestrator -- 核心业务包 + +## 模块职责 + +项目唯一的 Python 包,包含全部后端业务逻辑:OpenAI 账号自动注册引擎、FastAPI REST API 服务、CPA / Sub2Api 双平台账号池维护、多邮箱提供商适配层,以及嵌入式 Web 前端静态资源。 + +## 入口与启动 + +| 入口 | 路径 | 说明 | +|------|------|------| +| Web 服务 | `__main__.py` -> `main()` | 启动 Uvicorn 服务器,监听 `0.0.0.0:18421`,加载 `server.app` | +| CLI 注册 | `register.py` -> `main()` | 命令行单次/循环注册模式,通过 argparse 接收参数 | +| 快捷脚本 | `../run.py` | 根据 `--cli` 标志分派到上述两种入口 | +| pip 命令 | `openai-pool` | pyproject.toml 注册的控制台入口,指向 `__main__:main` | + +## 对外接口 + +### REST API(server.py) + +FastAPI 应用提供 40+ 端点,核心分组: + +**任务控制** +- `POST /api/start` -- 启动注册任务(支持多线程、目标数量、代理配置) +- `POST /api/stop` -- 停止运行中的任务 + +**代理管理** +- `POST /api/proxy/save` / `GET /api/proxy` -- 保存/获取代理地址 +- `POST /api/check-proxy` -- 检测代理可用性 +- `GET /api/proxy-pool/config` / `POST /api/proxy-pool/config` -- 代理池配置 +- `POST /api/proxy-pool/test` -- 测试代理池取号 + +**配置管理** +- `GET /api/sync-config` / `POST /api/sync-config` -- Sub2Api 同步配置 +- `GET /api/pool/config` / `POST /api/pool/config` -- CPA 池配置 +- `GET /api/mail/config` / `POST /api/mail/config` -- 邮箱提供商配置 +- `POST /api/upload-mode` -- 上传策略切换(snapshot / decoupled) + +**Token 管理** +- `GET /api/tokens` -- 列出本地 Token 文件 +- `DELETE /api/tokens/{filename}` -- 删除单个 Token +- `POST /api/sync-now` -- 单个 Token 导入 Sub2Api +- `POST /api/sync-batch` -- 批量导入 + +**CPA 池** +- `GET /api/pool/status` -- CPA 池状态 +- `POST /api/pool/check` -- 探测 CPA 池 +- `POST /api/pool/maintain` -- 执行 CPA 维护 +- `POST /api/pool/auto` -- 开关自动维护 + +**Sub2Api 池** +- `GET /api/sub2api/accounts` -- 分页账号列表(支持状态/关键字筛选) +- `POST /api/sub2api/accounts/probe` -- 批量测活 +- `POST /api/sub2api/accounts/delete` -- 批量删除 +- `POST /api/sub2api/accounts/handle-exception` -- 异常处理 +- `GET /api/sub2api/pool/status` -- Sub2Api 池状态 +- `POST /api/sub2api/pool/maintain` -- Sub2Api 维护 +- `POST /api/sub2api/pool/dedupe` -- 重复账号去重 + +**实时通信** +- `GET /api/logs` -- SSE 事件流(结构化事件:task_snapshot、runtime_snapshot、stats、log、pool_status 等) + +### SSE 事件类型 + +| 事件类型 | 说明 | +|---------|------| +| `task_snapshot` | 任务全局状态快照 | +| `runtime_snapshot` | 多 Worker 运行时明细 | +| `stats` | 成功/失败计数统计 | +| `log` | 实时日志消息 | +| `pool_status` | CPA/Sub2Api 池状态变化 | + +## 关键依赖与配置 + +### Python 依赖 + +| 包 | 用途 | +|----|------| +| `fastapi` >= 0.110 | Web 框架 | +| `uvicorn[standard]` >= 0.27 | ASGI 服务器 | +| `curl-cffi` >= 0.6 | TLS 指纹伪装 HTTP 客户端(模拟 Chrome) | +| `aiohttp` >= 3.9 | 异步 HTTP(池维护探测) | +| `requests` >= 2.31 | 同步 HTTP 客户端 | +| `pydantic` | 请求体校验(FastAPI 内置) | + +### 配置文件 + +- `data/sync_config.json` -- 运行时配置(从 `config/sync_config.example.json` 生成) +- `data/state.json` -- 累计成功/失败计数持久化 +- `data/tokens/` -- 注册获取的 Token JSON 文件 + +### 核心配置项 + +| 配置键 | 类型 | 说明 | +|--------|------|------| +| `proxy` | str | 固定代理地址 | +| `auto_register` | bool | 池不足时自动注册 | +| `mail_providers` | list[str] | 启用的邮箱提供商列表 | +| `mail_strategy` | str | 邮箱路由策略:round_robin / random / failover | +| `base_url` / `email` / `password` | str | Sub2Api 平台连接信息 | +| `cpa_base_url` / `cpa_token` | str | CPA 平台连接信息 | +| `upload_mode` | str | 上传策略:snapshot(串行)/ decoupled(双平台同传)| +| `proxy_pool_*` | mixed | 代理池 API 配置 | +| `sub2api_maintain_actions` | dict | Sub2Api 维护动作开关 | + +## 数据模型 + +### Token 文件格式(data/tokens/*.json) + +注册成功后保存的 Token 文件包含 OAuth 凭证信息,用于后续导入平台。 + +### TaskState(server.py) + +全局单例,管理注册任务的完整生命周期: +- 多 Worker 线程管理与运行时快照 +- SSE 事件订阅/分发 +- 成功/失败计数与平台上传统计 +- 注册步骤追踪:check_proxy -> create_email -> oauth_init -> sentinel -> signup -> send_otp -> wait_otp -> verify_otp -> create_account -> workspace -> get_token + +### PoolMaintainer(pool_maintainer.py) + +CPA 平台维护器: +- `fetch_auth_files()` -- 获取全部 auth 文件 +- `get_pool_status()` -- 池状态统计 +- `probe_accounts_async()` -- 异步批量探测账号有效性 + +### Sub2ApiMaintainer(pool_maintainer.py) + +Sub2Api 平台维护器: +- `list_accounts()` / `_list_all_accounts()` -- 分页/全量列出账号 +- `get_dashboard_stats()` -- 仪表盘统计 +- 自动 token 刷新(401 -> re-login) + +### MailProvider 体系(mail_providers.py) + +抽象基类 + 4 种实现: + +| 类名 | 提供商 | 认证方式 | +|------|--------|---------| +| `MailTmProvider` | Mail.tm | Bearer Token | +| `MoeMailProvider` | MoeMail | API Key | +| `DuckMailProvider` | DuckMail | Bearer Token | +| `CloudflareTempEmailProvider` | Cloudflare Workers | JWT + Admin Password | + +`MultiMailRouter` -- 线程安全的多提供商路由器,支持轮询/随机/容错策略。 + +## 测试与质量 + +- **测试**:当前无测试套件 +- **类型检查**:无 mypy/pyright 配置 +- **Lint**:无 ruff/flake8 配置 +- **CI/CD**:无 + +**建议优先覆盖的测试场景**: +1. `mail_providers.py` -- 各提供商创建邮箱与 OTP 轮询的异常路径 +2. `register.py` -- OAuth 流程各步骤的错误处理与重试 +3. `server.py` -- 核心 API 端点的请求/响应校验 +4. `pool_maintainer.py` -- 池状态计算与维护动作 + +## 常见问题 (FAQ) + +**Q: server.py 文件为何如此庞大?** +A: 当前 server.py 约 3500+ 行,包含了全部 REST API 路由、TaskState 状态管理、平台交互逻辑、自动维护定时器等。建议后续拆分为路由模块、任务管理模块、平台交互模块等。 + +**Q: register.py 中 curl-cffi 的作用?** +A: 使用 `curl_cffi.requests` 而非标准 `requests`,可伪装 Chrome TLS 指纹,避免被 OpenAI / Cloudflare 反爬检测拦截。 + +**Q: 如何新增邮箱提供商?** +A: 继承 `MailProvider` 基类,实现 `create_mailbox()` 和 `wait_for_otp()` 方法,然后在 `create_provider_by_name()` 工厂函数中注册。 + +**Q: 双平台上传策略的区别?** +A: `snapshot` 模式按顺序先补 CPA 再补 Sub2Api;`decoupled` 模式让单个账号同时上传到两个平台。 + +## 相关文件清单 + +| 文件 | 行数(估) | 说明 | +|------|----------|------| +| `__init__.py` | 29 | 包初始化,路径常量定义 | +| `__main__.py` | 119 | Uvicorn 启动与优雅关闭 | +| `server.py` | 3550+ | FastAPI 服务,全部 API 与任务状态 | +| `register.py` | 1600+ | OpenAI OAuth 注册引擎 | +| `pool_maintainer.py` | 800+ | CPA / Sub2Api 池维护 | +| `mail_providers.py` | 809 | 邮箱提供商抽象与 4 种实现 | +| `static/index.html` | 695 | Web UI 页面结构 | +| `static/app.js` | 2200+ | 前端交互逻辑 | +| `static/style.css` | 2800+ | iOS Flat Design 样式 | + +## 变更记录 (Changelog) + +| 时间 | 操作 | 说明 | +|------|------|------| +| 2026-03-18 09:19:57 | 初始扫描 | 首次生成模块级 CLAUDE.md | diff --git a/openai_pool_orchestrator/__init__.py b/openai_pool_orchestrator/__init__.py new file mode 100755 index 0000000..5abdf1a --- /dev/null +++ b/openai_pool_orchestrator/__init__.py @@ -0,0 +1,28 @@ +""" +OpenAI Pool Orchestrator +======================== +自动化 OpenAI 账号注册、Token 管理与多平台账号池维护工具。 +""" + +__version__ = "2.0.0" +__author__ = "OpenAI Pool Orchestrator Contributors" + +import os +from pathlib import Path + +# 项目根目录(包目录的上一级) +PACKAGE_DIR = Path(__file__).parent +PROJECT_ROOT = PACKAGE_DIR.parent + +# 运行时数据目录 +DATA_DIR = PROJECT_ROOT / "data" +DATA_DIR.mkdir(exist_ok=True) + +TOKENS_DIR = DATA_DIR / "tokens" +TOKENS_DIR.mkdir(exist_ok=True) + +CONFIG_FILE = DATA_DIR / "sync_config.json" +STATE_FILE = DATA_DIR / "state.json" + +# 前端静态文件目录 +STATIC_DIR = PACKAGE_DIR / "static" diff --git a/openai_pool_orchestrator/__main__.py b/openai_pool_orchestrator/__main__.py new file mode 100755 index 0000000..49675c5 --- /dev/null +++ b/openai_pool_orchestrator/__main__.py @@ -0,0 +1,119 @@ +""" +允许通过 python -m openai_pool_orchestrator 启动服务。 +""" + +import os +import sys +import threading +from typing import Callable + +import uvicorn + +from . import __version__ + +GRACEFUL_SHUTDOWN_TIMEOUT = 5 +FORCE_EXIT_TIMEOUT = 3 + + +def _request_server_shutdown( + server: uvicorn.Server, + notify_shutdown: Callable[[], None], + *, + force: bool = False, + message: str | None = None, +) -> None: + if message: + print(f"\n{message}") + server.should_exit = True + if force: + server.force_exit = True + notify_shutdown() + + +def _install_windows_ctrl_handler( + server: uvicorn.Server, + notify_shutdown: Callable[[], None], +): + import ctypes + + kernel32 = ctypes.windll.kernel32 + shutting_down = threading.Event() + shutdown_finished = threading.Event() + + def _force_exit_after_timeout() -> None: + if shutdown_finished.wait(GRACEFUL_SHUTDOWN_TIMEOUT): + return + _request_server_shutdown( + server, + notify_shutdown, + force=True, + message="正在强制退出...", + ) + if shutdown_finished.wait(FORCE_EXIT_TIMEOUT): + return + os._exit(130) + + def _ctrl_handler(ctrl_type): + # CTRL_C_EVENT = 0, CTRL_BREAK_EVENT = 1 + if ctrl_type not in (0, 1): + return False + + if shutting_down.is_set(): + _request_server_shutdown( + server, + notify_shutdown, + force=True, + message=None if server.force_exit else "正在强制退出...", + ) + return True + + shutting_down.set() + _request_server_shutdown(server, notify_shutdown, message="正在退出...") + threading.Thread(target=_force_exit_after_timeout, daemon=True).start() + return True + + handler_routine = ctypes.WINFUNCTYPE(ctypes.c_int, ctypes.c_uint) + handler = handler_routine(_ctrl_handler) + kernel32.SetConsoleCtrlHandler(handler, True) + + def _cleanup() -> None: + shutdown_finished.set() + try: + kernel32.SetConsoleCtrlHandler(handler, False) + except Exception: + pass + + return _cleanup + + +def main() -> None: + print("=" * 50) + print(f" OpenAI Pool Orchestrator v{__version__}") + print(" 访问: http://localhost:18421") + print(" 按 Ctrl+C 可退出") + print("=" * 50) + + from .server import app, request_service_shutdown + + config = uvicorn.Config( + app, + host="0.0.0.0", + port=18421, + log_level="warning", + timeout_graceful_shutdown=GRACEFUL_SHUTDOWN_TIMEOUT, + ) + server = uvicorn.Server(config) + + cleanup_ctrl_handler = None + if sys.platform == "win32": + cleanup_ctrl_handler = _install_windows_ctrl_handler(server, request_service_shutdown) + + try: + server.run() + finally: + if cleanup_ctrl_handler is not None: + cleanup_ctrl_handler() + + +if __name__ == "__main__": + main() diff --git a/openai_pool_orchestrator/mail_providers.py b/openai_pool_orchestrator/mail_providers.py new file mode 100755 index 0000000..3965872 --- /dev/null +++ b/openai_pool_orchestrator/mail_providers.py @@ -0,0 +1,812 @@ +""" +MailProvider 抽象层 +支持 Mail.tm / MoeMail / DuckMail / 自定义兼容 API +""" + +from __future__ import annotations + +import itertools +import logging +import random +import re +import secrets +import string +import time +import threading +from abc import ABC, abstractmethod +from typing import Any, Dict, List, Optional, Tuple, Callable + +import requests as _requests +from requests.adapters import HTTPAdapter +import urllib3 +from urllib3.exceptions import InsecureRequestWarning +from urllib3.util.retry import Retry + +logger = logging.getLogger(__name__) +urllib3.disable_warnings(InsecureRequestWarning) + + +def _normalize_proxy_url(proxy: str) -> str: + value = str(proxy or "").strip() + if not value: + return "" + if "://" in value: + return value + if ":" in value: + return f"http://{value}" + return "" + + +class _ProxyAwareSession(_requests.Session): + def __init__( + self, + proxy: str = "", + proxy_selector: Optional[Callable[[], str]] = None, + ): + super().__init__() + self._default_proxy = _normalize_proxy_url(proxy) + self._proxy_selector = proxy_selector + + def request(self, method, url, **kwargs): + selected_proxy = "" + if self._proxy_selector: + try: + selected_proxy = _normalize_proxy_url(self._proxy_selector() or "") + except Exception: + selected_proxy = "" + if not selected_proxy: + selected_proxy = self._default_proxy + base_kwargs = dict(kwargs) + if selected_proxy and "proxies" not in base_kwargs: + base_kwargs["proxies"] = {"http": selected_proxy, "https": selected_proxy} + try: + return super().request(method, url, **base_kwargs) + except Exception: + # 动态代理失败时,自动回退固定代理(若有) + if ( + selected_proxy + and self._default_proxy + and selected_proxy != self._default_proxy + and "proxies" not in kwargs + ): + fallback_kwargs = dict(kwargs) + fallback_kwargs["proxies"] = {"http": self._default_proxy, "https": self._default_proxy} + return super().request(method, url, **fallback_kwargs) + raise + + +def _build_session(proxy: str = "", proxy_selector: Optional[Callable[[], str]] = None) -> _requests.Session: + s = _ProxyAwareSession(proxy, proxy_selector) + retry_total = 0 if proxy_selector else 2 + retry = Retry( + total=retry_total, + connect=retry_total, + read=retry_total, + status=retry_total, + backoff_factor=0.2, + status_forcelist=[429, 500, 502, 503, 504], + ) + adapter = HTTPAdapter(max_retries=retry) + s.mount("https://", adapter) + s.mount("http://", adapter) + fixed_proxy = _normalize_proxy_url(proxy) + if fixed_proxy and not proxy_selector: + s.proxies = {"http": fixed_proxy, "https": fixed_proxy} + return s + + +def _extract_code(content: str) -> Optional[str]: + if not content: + return None + m = re.search(r"background-color:\s*#F3F3F3[^>]*>[\s\S]*?(\d{6})[\s\S]*?

", content) + if m: + return m.group(1) + for pat in [ + r"Verification code:?\s*(\d{6})", + r"code is\s*(\d{6})", + r"Subject:.*?(\d{6})", + r">\s*(\d{6})\s*<", + r"(? Tuple[str, str]: + """返回 (email, auth_credential),auth_credential 是 bearer token 或 email_id""" + + @abstractmethod + def wait_for_otp( + self, + auth_credential: str, + email: str, + proxy: str = "", + proxy_selector: Optional[Callable[[], str]] = None, + timeout: int = 120, + stop_event: Optional[threading.Event] = None, + ) -> str: + """轮询获取6位验证码,超时返回空字符串""" + + def test_connection(self, proxy: str = "") -> Tuple[bool, str]: + """测试 API 连通性,返回 (success, message)""" + try: + email, cred = self.create_mailbox(proxy) + if email and cred: + return True, f"成功创建测试邮箱: {email}" + return False, "创建邮箱失败,请检查配置" + except Exception as e: + return False, f"连接失败: {e}" + + def close(self): + pass + + +# ==================== Mail.tm ==================== + +class MailTmProvider(MailProvider): + def __init__(self, api_base: str = "https://api.mail.tm"): + self.api_base = api_base.rstrip("/") + + def _headers(self, token: str = "", use_json: bool = False) -> Dict[str, str]: + h: Dict[str, str] = {"Accept": "application/json"} + if use_json: + h["Content-Type"] = "application/json" + if token: + h["Authorization"] = f"Bearer {token}" + return h + + def _get_domains(self, session: _requests.Session) -> List[str]: + resp = session.get(f"{self.api_base}/domains", headers=self._headers(), timeout=15, verify=False) + if resp.status_code != 200: + return [] + data = resp.json() + items = data if isinstance(data, list) else (data.get("hydra:member") or data.get("items") or []) + domains = [] + for item in items: + if not isinstance(item, dict): + continue + domain = str(item.get("domain") or "").strip() + if domain and item.get("isActive", True) and not item.get("isPrivate", False): + domains.append(domain) + return domains + + def create_mailbox( + self, + proxy: str = "", + proxy_selector: Optional[Callable[[], str]] = None, + ) -> Tuple[str, str]: + with _build_session(proxy, proxy_selector) as session: + domains = self._get_domains(session) + if not domains: + return "", "" + # 优先使用 duckmail.sbs 主域名,避免临时域名被 OpenAI 封禁 + _preferred = [d for d in domains if "duckmail" in d.lower()] + domain = random.choice(_preferred) if _preferred else random.choice(domains) + + for _ in range(5): + local = f"oc{secrets.token_hex(5)}" + email = f"{local}@{domain}" + password = secrets.token_urlsafe(18) + + resp = session.post( + f"{self.api_base}/accounts", + headers=self._headers(use_json=True), + json={"address": email, "password": password}, + timeout=15, verify=False, + ) + if resp.status_code not in (200, 201): + continue + + token_resp = session.post( + f"{self.api_base}/token", + headers=self._headers(use_json=True), + json={"address": email, "password": password}, + timeout=15, verify=False, + ) + if token_resp.status_code == 200: + token = str(token_resp.json().get("token") or "").strip() + if token: + return email, token + return "", "" + + def wait_for_otp( + self, + auth_credential: str, + email: str, + proxy: str = "", + proxy_selector: Optional[Callable[[], str]] = None, + timeout: int = 120, + stop_event: Optional[threading.Event] = None, + ) -> str: + with _build_session(proxy, proxy_selector) as session: + seen_ids: set = set() + start = time.time() + + while time.time() - start < timeout: + if stop_event and stop_event.is_set(): + return "" + try: + resp = session.get( + f"{self.api_base}/messages", + headers=self._headers(token=auth_credential), + timeout=15, verify=False, + ) + if resp.status_code != 200: + time.sleep(3) + continue + + data = resp.json() + messages = data if isinstance(data, list) else ( + data.get("hydra:member") or data.get("messages") or [] + ) + + for msg in messages: + if not isinstance(msg, dict): + continue + msg_id = str(msg.get("id") or msg.get("@id") or "").strip() + if not msg_id or msg_id in seen_ids: + continue + + if msg_id.startswith("/messages/"): + msg_id = msg_id.split("/")[-1] + + detail_resp = session.get( + f"{self.api_base}/messages/{msg_id}", + headers=self._headers(token=auth_credential), + timeout=15, verify=False, + ) + if detail_resp.status_code != 200: + continue + seen_ids.add(msg_id) + + mail_data = detail_resp.json() + sender = str(((mail_data.get("from") or {}).get("address") or "")).lower() + subject = str(mail_data.get("subject") or "") + intro = str(mail_data.get("intro") or "") + text = str(mail_data.get("text") or "") + html = mail_data.get("html") or "" + if isinstance(html, list): + html = "\n".join(str(x) for x in html) + content = "\n".join([subject, intro, text, str(html)]) + + if "openai" not in sender and "openai" not in content.lower(): + continue + + code = _extract_code(content) + if code: + return code + except Exception as exc: + logger.warning("Mail.tm 轮询验证码失败: %s", exc) + time.sleep(3) + return "" + + +# ==================== MoeMail ==================== + +class MoeMailProvider(MailProvider): + def __init__(self, api_base: str, api_key: str): + self.api_base = api_base.rstrip("/") + self.api_key = api_key + + def _headers(self) -> Dict[str, str]: + return {"X-API-Key": self.api_key} + + def _get_domain(self, session: _requests.Session) -> Optional[str]: + try: + resp = session.get( + f"{self.api_base}/api/config", + headers=self._headers(), timeout=10, verify=False, + ) + if resp.status_code == 200: + data = resp.json() + domains_str = data.get("emailDomains", "") + if domains_str: + domains = [d.strip() for d in domains_str.split(",") if d.strip()] + if domains: + return random.choice(domains) + except Exception as exc: + logger.warning("MoeMail 读取域名配置失败: %s", exc) + return None + + def create_mailbox( + self, + proxy: str = "", + proxy_selector: Optional[Callable[[], str]] = None, + ) -> Tuple[str, str]: + with _build_session(proxy, proxy_selector) as session: + domain = self._get_domain(session) + if not domain: + return "", "" + + chars = string.ascii_lowercase + string.digits + prefix = "".join(random.choice(chars) for _ in range(random.randint(8, 13))) + + try: + resp = session.post( + f"{self.api_base}/api/emails/generate", + json={"name": prefix, "domain": domain, "expiryTime": 0}, + headers=self._headers(), timeout=15, verify=False, + ) + if resp.status_code not in (200, 201): + return "", "" + data = resp.json() + email_id = data.get("id") + email = data.get("email") + if email_id and email: + return email, str(email_id) + except Exception as exc: + logger.warning("MoeMail 创建邮箱失败: %s", exc) + return "", "" + + def wait_for_otp( + self, + auth_credential: str, + email: str, + proxy: str = "", + proxy_selector: Optional[Callable[[], str]] = None, + timeout: int = 120, + stop_event: Optional[threading.Event] = None, + ) -> str: + with _build_session(proxy, proxy_selector) as session: + email_id = auth_credential + start = time.time() + + while time.time() - start < timeout: + if stop_event and stop_event.is_set(): + return "" + try: + resp = session.get( + f"{self.api_base}/api/emails/{email_id}", + headers=self._headers(), timeout=15, verify=False, + ) + if resp.status_code == 200: + messages = resp.json().get("messages") or [] + for msg in messages: + if not isinstance(msg, dict): + continue + msg_id = msg.get("id") + if not msg_id: + continue + detail_resp = session.get( + f"{self.api_base}/api/emails/{email_id}/{msg_id}", + headers=self._headers(), timeout=15, verify=False, + ) + if detail_resp.status_code == 200: + detail = detail_resp.json() + msg_obj = detail.get("message") or {} + content = msg_obj.get("content") or msg_obj.get("html") or "" + if not content: + content = detail.get("text") or detail.get("html") or "" + code = _extract_code(content) + if code: + return code + except Exception as exc: + logger.warning("MoeMail 轮询验证码失败: %s", exc) + time.sleep(3) + return "" + + +# ==================== DuckMail ==================== + +class DuckMailProvider(MailProvider): + def __init__(self, api_base: str = "https://api.duckmail.sbs", bearer_token: str = ""): + self.api_base = api_base.rstrip("/") + self.bearer_token = bearer_token + + def _auth_headers(self, token: str = "") -> Dict[str, str]: + h: Dict[str, str] = {"Accept": "application/json"} + if token: + h["Authorization"] = f"Bearer {token}" + return h + + def create_mailbox( + self, + proxy: str = "", + proxy_selector: Optional[Callable[[], str]] = None, + ) -> Tuple[str, str]: + with _build_session(proxy, proxy_selector) as session: + headers: Dict[str, str] = {"Content-Type": "application/json", "Accept": "application/json"} + if self.bearer_token: + headers["Authorization"] = f"Bearer {self.bearer_token}" + + try: + domains_resp = session.get(f"{self.api_base}/domains", headers={"Accept": "application/json"}, timeout=15, verify=False) + if domains_resp.status_code != 200: + return "", "" + data = domains_resp.json() + items = data if isinstance(data, list) else (data.get("hydra:member") or []) + domains = [str(i.get("domain") or "") for i in items if isinstance(i, dict) and i.get("domain") and i.get("isActive", True)] + if not domains: + return "", "" + # 优先使用 duckmail.sbs 主域名,避免临时域名被 OpenAI 封禁 + _preferred = [d for d in domains if "duckmail" in d.lower()] + domain = random.choice(_preferred) if _preferred else random.choice(domains) + + local = f"oc{secrets.token_hex(5)}" + email = f"{local}@{domain}" + password = secrets.token_urlsafe(18) + + resp = session.post( + f"{self.api_base}/accounts", + json={"address": email, "password": password}, + headers=headers, timeout=30, verify=False, + ) + if resp.status_code not in (200, 201): + return "", "" + + time.sleep(0.5) + token_resp = session.post( + f"{self.api_base}/token", + json={"address": email, "password": password}, + headers=headers, timeout=30, verify=False, + ) + if token_resp.status_code == 200: + mail_token = token_resp.json().get("token") + if mail_token: + return email, str(mail_token) + except Exception as exc: + logger.warning("DuckMail 创建邮箱失败: %s", exc) + return "", "" + + def wait_for_otp( + self, + auth_credential: str, + email: str, + proxy: str = "", + proxy_selector: Optional[Callable[[], str]] = None, + timeout: int = 120, + stop_event: Optional[threading.Event] = None, + ) -> str: + with _build_session(proxy, proxy_selector) as session: + seen_ids: set = set() + start = time.time() + + while time.time() - start < timeout: + if stop_event and stop_event.is_set(): + return "" + try: + resp = session.get( + f"{self.api_base}/messages", + headers=self._auth_headers(auth_credential), + timeout=30, verify=False, + ) + if resp.status_code == 200: + data = resp.json() + messages = data.get("hydra:member") or data.get("member") or data.get("data") or [] + for msg in (messages if isinstance(messages, list) else []): + if not isinstance(msg, dict): + continue + msg_id = msg.get("id") or msg.get("@id") + if not msg_id or msg_id in seen_ids: + continue + raw_id = str(msg_id).split("/")[-1] if str(msg_id).startswith("/") else str(msg_id) + + detail_resp = session.get( + f"{self.api_base}/messages/{raw_id}", + headers=self._auth_headers(auth_credential), + timeout=30, verify=False, + ) + if detail_resp.status_code == 200: + seen_ids.add(msg_id) + detail = detail_resp.json() + content = detail.get("text") or detail.get("html") or "" + code = _extract_code(content) + if code: + return code + except Exception as exc: + logger.warning("DuckMail 轮询验证码失败: %s", exc) + time.sleep(3) + return "" + + +# ==================== Cloudflare Temp Email ==================== + +class CloudflareTempEmailProvider(MailProvider): + def __init__(self, api_base: str = "", admin_password: str = "", domain: str = ""): + self.api_base = api_base.rstrip("/") + self.admin_password = admin_password + self.domain = str(domain).strip() + # 使用线程本地 token,避免多线程下邮箱 token 串用。 + self._tls = threading.local() + + def _get_random_domain(self) -> str: + if not self.domain: + return "" + # 尝试按照 JSON 数组解析 + if self.domain.startswith("[") and self.domain.endswith("]"): + try: + import json + domain_list = json.loads(self.domain) + if isinstance(domain_list, list) and domain_list: + return random.choice([str(d).strip() for d in domain_list if str(d).strip()]) + except Exception: + pass + # 按照逗号分隔解析 + if "," in self.domain: + parts = [d.strip() for d in self.domain.split(",") if d.strip()] + if parts: + return random.choice(parts) + return self.domain + + @staticmethod + def _message_matches_email(msg: Dict[str, Any], target_email: str) -> bool: + target = str(target_email or "").strip().lower() + if not target: + return True + + def _extract_text_candidates(value: Any) -> List[str]: + out: List[str] = [] + if isinstance(value, str): + out.append(value) + elif isinstance(value, dict): + for k in ("address", "email", "name", "value"): + if value.get(k): + out.extend(_extract_text_candidates(value.get(k))) + elif isinstance(value, list): + for item in value: + out.extend(_extract_text_candidates(item)) + return out + + candidates: List[str] = [] + for key in ("to", "mailTo", "receiver", "receivers", "address", "email", "envelope_to"): + if key in msg: + candidates.extend(_extract_text_candidates(msg.get(key))) + if not candidates: + return True + target_lower = target.lower() + for raw in candidates: + text = str(raw or "").strip().lower() + if not text: + continue + if target_lower in text: + return True + return False + + def create_mailbox( + self, + proxy: str = "", + proxy_selector: Optional[Callable[[], str]] = None, + ) -> Tuple[str, str]: + if not self.api_base or not self.admin_password or not self.domain: + return "", "" + + with _build_session(proxy, proxy_selector) as session: + try: + # 生成5位字母 + 1-3位数字 + 1-3位字母的随机名 + letters1 = ''.join(random.choices(string.ascii_lowercase, k=5)) + numbers = ''.join(random.choices(string.digits, k=random.randint(1, 3))) + letters2 = ''.join(random.choices(string.ascii_lowercase, k=random.randint(1, 3))) + name = letters1 + numbers + letters2 + + target_domain = self._get_random_domain() + if not target_domain: + return "", "" + + resp = session.post( + f"{self.api_base}/admin/new_address", + json={ + "enablePrefix": True, + "name": name, + "domain": target_domain, + }, + headers={ + "x-admin-auth": self.admin_password, + "Content-Type": "application/json" + }, + timeout=30, verify=False, + ) + if resp.status_code == 200: + data = resp.json() + email = data.get("address") + jwt_token = data.get("jwt") + if email and jwt_token: + self._tls.jwt_token = jwt_token + return email, jwt_token + except Exception as exc: + logger.warning("Cloudflare 临时邮箱创建失败: %s", exc) + return "", "" + + def wait_for_otp( + self, + auth_credential: str, + email: str, + proxy: str = "", + proxy_selector: Optional[Callable[[], str]] = None, + timeout: int = 120, + stop_event: Optional[threading.Event] = None, + ) -> str: + token = str(auth_credential or "").strip() or str(getattr(self._tls, "jwt_token", "") or "").strip() + if not token: + return "" + print(f"[CFMail] wait_for_otp 进入! email={email}, api_base={self.api_base}, jwt前16={token[:16] if token else 'EMPTY'}", flush=True) + with _build_session(proxy, proxy_selector) as session: + seen_ids: set = set() + start = time.time() + poll_count = 0 + + while time.time() - start < timeout: + if stop_event and stop_event.is_set(): + print("[CFMail] stop_event 已触发,退出", flush=True) + return "" + try: + poll_count += 1 + url = f"{self.api_base}/api/mails?limit=10&offset=0" + resp = session.get( + url, + headers={ + "Authorization": f"Bearer {token}", + "Content-Type": "application/json" + }, + timeout=30, verify=False, + ) + print(f"[CFMail] 轮询#{poll_count} status={resp.status_code}, body前200={str(resp.text or '')[:200]}", flush=True) + if resp.status_code == 200: + try: + data = resp.json() + except Exception as je: + print(f"[CFMail] JSON解析失败: {je}", flush=True) + time.sleep(3) + continue + # API 返回字典 {"results": [...], "count": 0},需正确提取 + if isinstance(data, dict): + messages = data.get("results") or [] + elif isinstance(data, list): + messages = data + else: + messages = [] + print(f"[CFMail] 解析到 {len(messages)} 条邮件", flush=True) + for msg in messages: + if not isinstance(msg, dict): + continue + if not self._message_matches_email(msg, email): + continue + msg_id = msg.get("id") + if not msg_id or msg_id in seen_ids: + continue + seen_ids.add(msg_id) + + content = msg.get("text") or msg.get("html") or "" + # Cloudflare Temp Email 将邮件原文放在 raw 字段(MIME 格式) + if not content and msg.get("raw"): + try: + import email as _email_mod + from email import policy + parsed = _email_mod.message_from_string(msg["raw"], policy=policy.default) + # 优先取纯文本 + body = parsed.get_body(preferencelist=('plain', 'html')) + if body: + content = body.get_content() or "" + if not content: + # 回退:遍历所有 part + for part in parsed.walk(): + ctype = part.get_content_type() + if ctype in ("text/plain", "text/html"): + payload = part.get_content() + if payload: + content = str(payload) + break + except Exception as parse_err: + print(f"[CFMail] MIME解析失败,回退raw: {parse_err}", flush=True) + content = msg.get("raw", "") + print(f"[CFMail] 邮件id={msg_id}, 内容前200={content[:200]}", flush=True) + code = _extract_code(content) + if code: + print(f"[CFMail] 成功提取验证码: {code}", flush=True) + return code + except Exception as e: + print(f"[CFMail] 轮询异常: {e}", flush=True) + time.sleep(3) + print("[CFMail] wait_for_otp 超时, 未获取到验证码", flush=True) + return "" + + +# ==================== 多提供商路由 ==================== + + +class MultiMailRouter: + """线程安全的多邮箱提供商路由器,支持轮询/随机/容错策略""" + + def __init__(self, config: Dict[str, Any]): + providers_list: List[str] = config.get("mail_providers") or [] + provider_configs: Dict[str, Dict] = config.get("mail_provider_configs") or {} + self.strategy: str = config.get("mail_strategy", "round_robin") + + if not providers_list: + legacy = config.get("mail_provider", "mailtm") + providers_list = [legacy] + provider_configs = {legacy: config.get("mail_config") or {}} + + self._provider_names: List[str] = [] + self._providers: Dict[str, MailProvider] = {} + self._failures: Dict[str, int] = {} + self._lock = threading.RLock() + self._counter = itertools.count() + + for name in providers_list: + try: + p = create_provider_by_name(name, provider_configs.get(name, {})) + self._provider_names.append(name) + self._providers[name] = p + self._failures[name] = 0 + except Exception as e: + logger.warning("创建邮箱提供商 %s 失败: %s", name, e) + + if not self._providers: + if providers_list: + raise RuntimeError(f"邮箱提供商配置无效: {', '.join(str(n) for n in providers_list)}") + fallback = create_provider_by_name("mailtm", {}) + self._provider_names = ["mailtm"] + self._providers = {"mailtm": fallback} + self._failures = {"mailtm": 0} + + def next_provider(self) -> Tuple[str, MailProvider]: + with self._lock: + names = self._provider_names + if not names: + raise RuntimeError("无可用邮箱提供商") + + if self.strategy == "random": + name = random.choice(names) + elif self.strategy == "failover": + name = min(names, key=lambda n: self._failures.get(n, 0)) + else: + idx = next(self._counter) % len(names) + name = names[idx] + return name, self._providers[name] + + def providers(self) -> List[Tuple[str, MailProvider]]: + with self._lock: + return [(n, self._providers[n]) for n in self._provider_names] + + def report_success(self, provider_name: str) -> None: + with self._lock: + self._failures[provider_name] = max(0, self._failures.get(provider_name, 0) - 1) + + def report_failure(self, provider_name: str) -> None: + with self._lock: + self._failures[provider_name] = self._failures.get(provider_name, 0) + 1 + + +# ==================== 工厂函数 ==================== + + +def create_provider_by_name(provider_type: str, mail_cfg: Dict[str, Any]) -> MailProvider: + """根据提供商名称和单独配置创建实例""" + provider_type = provider_type.lower().strip() + api_base = str(mail_cfg.get("api_base", "")).strip() + + if provider_type == "moemail": + return MoeMailProvider( + api_base=api_base or "https://your-moemail-api.example.com", + api_key=str(mail_cfg.get("api_key", "")).strip(), + ) + elif provider_type == "duckmail": + return DuckMailProvider( + api_base=api_base or "https://api.duckmail.sbs", + bearer_token=str(mail_cfg.get("bearer_token", "")).strip(), + ) + elif provider_type == "cloudflare_temp_email": + return CloudflareTempEmailProvider( + api_base=api_base, + admin_password=str(mail_cfg.get("admin_password", "")).strip(), + domain=str(mail_cfg.get("domain", "")).strip(), + ) + elif provider_type == "mailtm": + return MailTmProvider(api_base=api_base or "https://api.mail.tm") + raise ValueError(f"未知邮箱提供商: {provider_type}") + + +def create_provider(config: Dict[str, Any]) -> MailProvider: + """兼容旧配置格式的工厂函数""" + provider_type = str(config.get("mail_provider", "mailtm")).lower() + mail_cfg = config.get("mail_config") or {} + return create_provider_by_name(provider_type, mail_cfg) diff --git a/openai_pool_orchestrator/pool_maintainer.py b/openai_pool_orchestrator/pool_maintainer.py new file mode 100755 index 0000000..6bd5a6d --- /dev/null +++ b/openai_pool_orchestrator/pool_maintainer.py @@ -0,0 +1,1061 @@ +""" +账号池维护模块 +支持 CPA 平台和 Sub2Api 平台的探测、清理、计数和补号 +""" + +from __future__ import annotations + +import asyncio +import json +import logging +import re +import threading +import time +from concurrent.futures import ThreadPoolExecutor, as_completed +from datetime import datetime +from typing import Any, Dict, List, Optional +from urllib.parse import quote + +import requests as _requests +from requests.adapters import HTTPAdapter +from urllib3.util.retry import Retry + +try: + import aiohttp +except ImportError: + aiohttp = None + +logger = logging.getLogger(__name__) + +DEFAULT_MGMT_UA = "codex_cli_rs/0.76.0 (Debian 13.0.0; x86_64) WindowsTerminal" + + +def _mgmt_headers(token: str) -> Dict[str, str]: + return {"Authorization": f"Bearer {token}", "Accept": "application/json"} + + +def _build_session(proxy: str = "") -> _requests.Session: + s = _requests.Session() + retry = Retry(total=3, backoff_factor=1, status_forcelist=[429, 500, 502, 503, 504]) + adapter = HTTPAdapter(max_retries=retry) + s.mount("https://", adapter) + s.mount("http://", adapter) + if proxy: + s.proxies = {"http": proxy, "https": proxy} + return s + + +def _get_item_type(item: Dict[str, Any]) -> str: + return str(item.get("type") or item.get("typo") or "") + + +def _safe_json(text: str) -> Dict[str, Any]: + try: + return json.loads(text) + except Exception: + return {} + + +def _extract_account_id(item: Dict[str, Any]) -> Optional[str]: + for key in ("chatgpt_account_id", "chatgptAccountId", "account_id", "accountId"): + val = item.get(key) + if val: + return str(val) + return None + + +def _parse_time_to_epoch(raw: Any) -> float: + text = str(raw or "").strip() + if not text: + return 0.0 + iso_text = text[:-1] + "+00:00" if text.endswith("Z") else text + try: + return datetime.fromisoformat(iso_text).timestamp() + except Exception: + pass + for fmt in ("%Y-%m-%d %H:%M:%S", "%Y-%m-%dT%H:%M:%S"): + try: + return datetime.strptime(text, fmt).timestamp() + except Exception: + continue + return 0.0 + + +class PoolMaintainer: + def __init__( + self, + cpa_base_url: str, + cpa_token: str, + target_type: str = "codex", + min_candidates: int = 800, + used_percent_threshold: int = 95, + user_agent: str = DEFAULT_MGMT_UA, + ): + self.base_url = cpa_base_url.rstrip("/") + self.token = cpa_token + self.target_type = target_type + self.min_candidates = min_candidates + self.used_percent_threshold = used_percent_threshold + self.user_agent = user_agent + + def fetch_auth_files(self, timeout: int = 15) -> List[Dict[str, Any]]: + resp = _requests.get( + f"{self.base_url}/v0/management/auth-files", + headers=_mgmt_headers(self.token), + timeout=timeout, + ) + resp.raise_for_status() + raw = resp.json() + data = raw if isinstance(raw, dict) else {} + files = data.get("files", []) + return files if isinstance(files, list) else [] + + def get_pool_status(self, timeout: int = 15) -> Dict[str, Any]: + try: + files = self.fetch_auth_files(timeout) + candidates = [f for f in files if _get_item_type(f).lower() == self.target_type.lower()] + total = len(files) + cand_count = len(candidates) + return { + "total": total, + "candidates": cand_count, + "error_count": max(0, total - cand_count), + "threshold": self.min_candidates, + "healthy": cand_count >= self.min_candidates, + "percent": round(cand_count / self.min_candidates * 100, 1) if self.min_candidates > 0 else 100, + "last_checked": time.strftime("%Y-%m-%d %H:%M:%S"), + "error": None, + } + except Exception as e: + return { + "total": 0, + "candidates": 0, + "error_count": 0, + "threshold": self.min_candidates, + "healthy": False, + "percent": 0, + "last_checked": time.strftime("%Y-%m-%d %H:%M:%S"), + "error": str(e), + } + + def test_connection(self, timeout: int = 10) -> Dict[str, Any]: + try: + files = self.fetch_auth_files(timeout) + candidates = [f for f in files if _get_item_type(f).lower() == self.target_type.lower()] + return { + "ok": True, + "total": len(files), + "candidates": len(candidates), + "message": f"连接成功,共 {len(files)} 个账号,{len(candidates)} 个 {self.target_type} 账号", + } + except Exception as e: + return {"ok": False, "total": 0, "candidates": 0, "message": f"连接失败: {e}"} + + async def probe_accounts_async( + self, workers: int = 20, timeout: int = 10, retries: int = 1, + ) -> Dict[str, Any]: + if aiohttp is None: + raise RuntimeError("需要安装 aiohttp: pip install aiohttp") + + files = self.fetch_auth_files(timeout) + candidates = [f for f in files if _get_item_type(f).lower() == self.target_type.lower()] + + if not candidates: + return {"total": len(files), "candidates": 0, "invalid": [], "files": files} + + semaphore = asyncio.Semaphore(max(1, workers)) + connector = aiohttp.TCPConnector(limit=max(1, workers)) + client_timeout = aiohttp.ClientTimeout(total=max(1, timeout)) + + async def probe_one(session: aiohttp.ClientSession, item: Dict[str, Any]) -> Dict[str, Any]: + auth_index = item.get("auth_index") + name = item.get("name") or item.get("id") + result = { + "name": name, + "auth_index": auth_index, + "invalid_401": False, + "invalid_used_percent": False, + "used_percent": None, + "error": None, + } + if not auth_index: + result["error"] = "missing auth_index" + return result + + account_id = _extract_account_id(item) + call_header = { + "Authorization": "Bearer $TOKEN$", + "Content-Type": "application/json", + "User-Agent": self.user_agent, + } + if account_id: + call_header["Chatgpt-Account-Id"] = account_id + + payload = { + "authIndex": auth_index, + "method": "GET", + "url": "https://chatgpt.com/backend-api/wham/usage", + "header": call_header, + } + + for attempt in range(retries + 1): + try: + async with semaphore: + async with session.post( + f"{self.base_url}/v0/management/api-call", + headers={**_mgmt_headers(self.token), "Content-Type": "application/json"}, + json=payload, + timeout=timeout, + ) as resp: + text = await resp.text() + if resp.status >= 400: + raise RuntimeError(f"HTTP {resp.status}: {text[:200]}") + data = _safe_json(text) + sc = data.get("status_code") + result["invalid_401"] = sc == 401 + if sc == 200: + try: + body_data = _safe_json(data.get("body", "")) + used_pct = (body_data.get("rate_limit", {}).get("primary_window", {}).get("used_percent")) + if used_pct is not None: + result["used_percent"] = used_pct + result["invalid_used_percent"] = used_pct >= self.used_percent_threshold + except Exception: + pass + return result + except Exception as e: + result["error"] = str(e) + if attempt >= retries: + return result + return result + + async def delete_one(session: aiohttp.ClientSession, name: str) -> Dict[str, Any]: + encoded = quote(name, safe="") + try: + async with semaphore: + async with session.delete( + f"{self.base_url}/v0/management/auth-files?name={encoded}", + headers=_mgmt_headers(self.token), + timeout=timeout, + ) as resp: + text = await resp.text() + data = _safe_json(text) + ok = resp.status == 200 and data.get("status") == "ok" + return {"name": name, "deleted": ok} + except Exception: + return {"name": name, "deleted": False} + + invalid_list = [] + async with aiohttp.ClientSession(connector=connector, timeout=client_timeout, trust_env=True) as session: + tasks = [asyncio.create_task(probe_one(session, item)) for item in candidates] + for task in asyncio.as_completed(tasks): + result = await task + if result.get("invalid_401") or result.get("invalid_used_percent"): + invalid_list.append(result) + + return { + "total": len(files), + "candidates": len(candidates), + "invalid": invalid_list, + "files": files, + } + + async def clean_invalid_async(self, workers: int = 20, timeout: int = 10, retries: int = 1) -> Dict[str, Any]: + if aiohttp is None: + raise RuntimeError("需要安装 aiohttp: pip install aiohttp") + + probe_result = await self.probe_accounts_async(workers, timeout, retries) + invalid = probe_result["invalid"] + names = [str(r["name"]) for r in invalid if r.get("name")] + + deleted_ok = 0 + deleted_fail = 0 + + if names: + semaphore = asyncio.Semaphore(max(1, workers)) + connector = aiohttp.TCPConnector(limit=max(1, workers)) + client_timeout = aiohttp.ClientTimeout(total=max(1, timeout)) + + async with aiohttp.ClientSession(connector=connector, timeout=client_timeout, trust_env=True) as session: + async def do_delete(name: str) -> bool: + encoded = quote(name, safe="") + try: + async with semaphore: + async with session.delete( + f"{self.base_url}/v0/management/auth-files?name={encoded}", + headers=_mgmt_headers(self.token), + timeout=timeout, + ) as resp: + text = await resp.text() + data = _safe_json(text) + return resp.status == 200 and data.get("status") == "ok" + except Exception: + return False + + tasks = [asyncio.create_task(do_delete(n)) for n in names] + for task in asyncio.as_completed(tasks): + if await task: + deleted_ok += 1 + else: + deleted_fail += 1 + + return { + "total": probe_result["total"], + "candidates": probe_result["candidates"], + "invalid_count": len(invalid), + "deleted_ok": deleted_ok, + "deleted_fail": deleted_fail, + } + + def probe_and_clean_sync(self, workers: int = 20, timeout: int = 10, retries: int = 1) -> Dict[str, Any]: + return asyncio.run(self.clean_invalid_async(workers, timeout, retries)) + + def calculate_gap(self, current_candidates: Optional[int] = None) -> int: + if current_candidates is None: + status = self.get_pool_status() + if status.get("error"): + raise RuntimeError(f"CPA 池状态查询失败: {status['error']}") + current_candidates = status["candidates"] + gap = self.min_candidates - current_candidates + return max(0, gap) + + def upload_token(self, filename: str, token_data: Dict[str, Any], proxy: str = "") -> bool: + if not self.base_url or not self.token: + return False + content = json.dumps(token_data, ensure_ascii=False).encode("utf-8") + files = {"file": (filename, content, "application/json")} + headers = {"Authorization": f"Bearer {self.token}"} + + with _build_session(proxy) as session: + for attempt in range(3): + try: + resp = session.post( + f"{self.base_url}/v0/management/auth-files", + files=files, headers=headers, verify=False, timeout=30, + ) + if resp.status_code in (200, 201, 204): + return True + except Exception: + pass + if attempt < 2: + time.sleep(2 ** attempt) + return False + + +class Sub2ApiMaintainer: + """Sub2Api 平台池维护 — 通过 Admin API 管理账号池""" + + def __init__( + self, + base_url: str, + bearer_token: str, + min_candidates: int = 200, + email: str = "", + password: str = "", + ): + self.base_url = base_url.rstrip("/") + self.bearer_token = bearer_token + self.min_candidates = min_candidates + self.email = email + self.password = password + self._auth_lock = threading.Lock() + + def _headers(self) -> Dict[str, str]: + return { + "Authorization": f"Bearer {self.bearer_token}", + "Accept": "application/json", + "Content-Type": "application/json", + } + + def _login(self) -> str: + with _build_session() as session: + resp = session.post( + f"{self.base_url}/api/v1/auth/login", + json={"email": self.email, "password": self.password}, + timeout=15, + ) + resp.raise_for_status() + data = resp.json() + token = ( + data.get("token") + or data.get("access_token") + or (data.get("data") or {}).get("token") + or (data.get("data") or {}).get("access_token") + or "" + ) + if token: + self.bearer_token = token + return token + + def _request(self, method: str, path: str, **kwargs) -> _requests.Response: + kwargs.setdefault("timeout", 15) + url = f"{self.base_url}{path}" + with _build_session() as session: + resp = session.request(method, url, headers=self._headers(), **kwargs) + if resp.status_code == 401 and self.email and self.password: + current_token = self.bearer_token + with self._auth_lock: + if self.bearer_token == current_token: + self._login() + refreshed_token = self.bearer_token + if refreshed_token or self.bearer_token != current_token: + resp = session.request(method, url, headers=self._headers(), **kwargs) + return resp + resp = session.request(method, url, headers=self._headers(), **kwargs) + return resp + + def get_dashboard_stats(self, timeout: int = 15) -> Dict[str, Any]: + resp = self._request( + "GET", "/api/v1/admin/dashboard/stats", + params={"timezone": "Asia/Shanghai"}, timeout=timeout, + ) + resp.raise_for_status() + data = resp.json() + return data.get("data") if isinstance(data.get("data"), dict) else data + + def list_accounts( + self, page: int = 1, page_size: int = 100, timeout: int = 15, + ) -> Dict[str, Any]: + params = { + "page": page, "page_size": page_size, + "platform": "openai", "type": "oauth", + } + resp = self._request( + "GET", "/api/v1/admin/accounts", + params=params, timeout=timeout, + ) + resp.raise_for_status() + data = resp.json() + return data.get("data") if isinstance(data.get("data"), dict) else data + + def _list_all_accounts(self, timeout: int = 15, page_size: int = 100) -> List[Dict[str, Any]]: + all_accounts: List[Dict[str, Any]] = [] + page = 1 + while True: + data = self.list_accounts(page=page, page_size=page_size, timeout=timeout) + items = data.get("items") or [] + if not isinstance(items, list): + items = [] + all_accounts.extend([i for i in items if isinstance(i, dict)]) + if not items or len(items) < page_size: + break + total = data.get("total") + if isinstance(total, int) and total > 0 and len(all_accounts) >= total: + break + page += 1 + return all_accounts + + def _account_identity(self, item: Dict[str, Any]) -> Dict[str, str]: + email = "" + rt = "" + extra = item.get("extra") + if isinstance(extra, dict): + email = str(extra.get("email") or "").strip().lower() + if not email: + name = str(item.get("name") or "").strip().lower() + if "@" in name: + email = name + creds = item.get("credentials") + if isinstance(creds, dict): + rt = str(creds.get("refresh_token") or "").strip() + return {"email": email, "refresh_token": rt} + + @staticmethod + def _account_sort_key(item: Dict[str, Any]) -> tuple[float, int]: + updated = _parse_time_to_epoch(item.get("updated_at") or item.get("updatedAt")) + try: + item_id = int(item.get("id") or 0) + except (TypeError, ValueError): + item_id = 0 + return (updated, item_id) + + @staticmethod + def _normalize_account_id(raw: Any) -> Optional[int]: + try: + account_id = int(raw) + except (TypeError, ValueError): + return None + if account_id <= 0: + return None + return account_id + + @staticmethod + def _is_abnormal_status(status: Any) -> bool: + return str(status or "").strip().lower() in ("error", "disabled") + + def _build_dedupe_plan(self, all_accounts: List[Dict[str, Any]], details_limit: int = 120) -> Dict[str, Any]: + id_to_account: Dict[int, Dict[str, Any]] = {} + parent: Dict[int, int] = {} + key_to_ids: Dict[str, List[int]] = {} + + for item in all_accounts: + acc_id = self._normalize_account_id(item.get("id")) + if acc_id is None: + continue + id_to_account[acc_id] = item + parent[acc_id] = acc_id + + identity = self._account_identity(item) + email = identity["email"] + refresh_token = identity["refresh_token"] + if email: + key_to_ids.setdefault(f"email:{email}", []).append(acc_id) + if refresh_token: + key_to_ids.setdefault(f"rt:{refresh_token}", []).append(acc_id) + + def find(x: int) -> int: + root = x + while parent[root] != root: + root = parent[root] + while parent[x] != x: + nxt = parent[x] + parent[x] = root + x = nxt + return root + + def union(a: int, b: int) -> None: + ra = find(a) + rb = find(b) + if ra != rb: + parent[rb] = ra + + for ids in key_to_ids.values(): + if len(ids) > 1: + head = ids[0] + for acc_id in ids[1:]: + union(head, acc_id) + + components: Dict[int, List[int]] = {} + for acc_id in id_to_account.keys(): + root = find(acc_id) + components.setdefault(root, []).append(acc_id) + + duplicate_groups = [ids for ids in components.values() if len(ids) > 1] + delete_ids: List[int] = [] + group_details: List[Dict[str, Any]] = [] + + for group_ids in duplicate_groups: + group_items = [id_to_account[i] for i in group_ids] + keep_item = max(group_items, key=self._account_sort_key) + keep_id = self._normalize_account_id(keep_item.get("id")) or 0 + group_delete_ids = sorted([i for i in group_ids if i != keep_id], reverse=True) + delete_ids.extend(group_delete_ids) + + if len(group_details) < details_limit: + emails_set = set() + for it in group_items: + identity = self._account_identity(it) + if identity["email"]: + emails_set.add(identity["email"]) + emails = sorted(emails_set) + group_details.append({ + "keep_id": keep_id, + "delete_ids": group_delete_ids, + "size": len(group_ids), + "emails": emails, + }) + + return { + "duplicate_groups": len(duplicate_groups), + "duplicate_accounts": sum(len(g) for g in duplicate_groups), + "delete_ids": delete_ids, + "groups_preview": group_details, + "truncated_groups": max(0, len(duplicate_groups) - len(group_details)), + } + + def list_account_inventory(self, timeout: int = 15) -> Dict[str, Any]: + all_accounts = self._list_all_accounts(timeout=timeout, page_size=100) + dedupe_plan = self._build_dedupe_plan( + all_accounts, + details_limit=max(1, len(all_accounts)), + ) + duplicate_delete_ids = { + int(account_id) + for account_id in (dedupe_plan.get("delete_ids") or []) + if isinstance(account_id, int) + } + duplicate_map: Dict[int, Dict[str, Any]] = {} + for group in dedupe_plan.get("groups_preview") or []: + keep_id = self._normalize_account_id(group.get("keep_id")) + delete_ids = [ + account_id + for account_id in ( + self._normalize_account_id(item) + for item in (group.get("delete_ids") or []) + ) + if account_id is not None + ] + group_ids = ([keep_id] if keep_id is not None else []) + delete_ids + group_size = max(1, int(group.get("size") or len(group_ids) or 1)) + emails = [str(email).strip().lower() for email in (group.get("emails") or []) if str(email).strip()] + for account_id in group_ids: + duplicate_map[account_id] = { + "group_size": group_size, + "keep_id": keep_id, + "delete_candidate": account_id in duplicate_delete_ids, + "emails": emails, + } + + items: List[Dict[str, Any]] = [] + abnormal_count = 0 + for raw_item in sorted(all_accounts, key=self._account_sort_key, reverse=True): + account_id = self._normalize_account_id(raw_item.get("id")) + if account_id is None: + continue + identity = self._account_identity(raw_item) + status = str(raw_item.get("status") or "").strip().lower() or "unknown" + if self._is_abnormal_status(status): + abnormal_count += 1 + duplicate_info = duplicate_map.get(account_id) or {} + items.append({ + "id": account_id, + "name": str(raw_item.get("name") or "").strip(), + "email": identity.get("email") or str(raw_item.get("name") or "").strip(), + "status": status, + "updated_at": raw_item.get("updated_at") or raw_item.get("updatedAt") or "", + "created_at": raw_item.get("created_at") or raw_item.get("createdAt") or "", + "is_duplicate": bool(duplicate_info), + "duplicate_group_size": int(duplicate_info.get("group_size") or 0), + "duplicate_keep": duplicate_info.get("keep_id") == account_id, + "duplicate_delete_candidate": bool(duplicate_info.get("delete_candidate")), + "duplicate_emails": duplicate_info.get("emails") or [], + }) + + return { + "total": len(items), + "error_count": abnormal_count, + "duplicate_groups": int(dedupe_plan.get("duplicate_groups", 0)), + "duplicate_accounts": int(dedupe_plan.get("duplicate_accounts", 0)), + "items": items, + } + + def _refresh_accounts_parallel(self, account_ids: List[int], timeout: int = 30, workers: int = 8) -> Dict[str, List[int]]: + success_ids: List[int] = [] + failed_ids: List[int] = [] + ids = [i for i in account_ids if isinstance(i, int) and i > 0] + if not ids: + return {"success_ids": success_ids, "failed_ids": failed_ids} + + pool_workers = max(1, min(workers, 16, len(ids))) + with ThreadPoolExecutor(max_workers=pool_workers) as executor: + future_to_id = { + executor.submit(self.refresh_account, account_id, timeout=timeout): account_id + for account_id in ids + } + for future in as_completed(future_to_id): + account_id = future_to_id[future] + try: + ok = bool(future.result()) + except Exception: + ok = False + if ok: + success_ids.append(account_id) + else: + failed_ids.append(account_id) + return {"success_ids": success_ids, "failed_ids": failed_ids} + + def _delete_accounts_parallel(self, account_ids: List[int], timeout: int = 15, workers: int = 12) -> Dict[str, Any]: + deleted_ok_ids: List[int] = [] + failed_ids: List[int] = [] + unique_ids = sorted({i for i in account_ids if isinstance(i, int) and i > 0}, reverse=True) + if not unique_ids: + return {"deleted_ok": 0, "deleted_fail": 0, "deleted_ok_ids": deleted_ok_ids, "failed_ids": failed_ids} + + pool_workers = max(1, min(workers, 24, len(unique_ids))) + with ThreadPoolExecutor(max_workers=pool_workers) as executor: + future_to_id = { + executor.submit(self.delete_account, account_id, timeout=timeout): account_id + for account_id in unique_ids + } + for future in as_completed(future_to_id): + account_id = future_to_id[future] + try: + ok = bool(future.result()) + except Exception: + ok = False + if ok: + deleted_ok_ids.append(account_id) + else: + failed_ids.append(account_id) + + return { + "deleted_ok": len(deleted_ok_ids), + "deleted_fail": len(failed_ids), + "deleted_ok_ids": deleted_ok_ids, + "failed_ids": failed_ids, + } + + def dedupe_duplicate_accounts(self, timeout: int = 15, dry_run: bool = True, details_limit: int = 120) -> Dict[str, Any]: + """ + 清理 Sub2Api 中 OpenAI OAuth 重复账号(按 email 或 refresh_token 判重)。 + - 同一连通重复组保留“最新”账号(updated_at 优先,其次 id 最大)。 + - dry_run=True 时仅预览,不执行删除。 + """ + all_accounts = self._list_all_accounts(timeout=timeout, page_size=100) + dedupe_plan = self._build_dedupe_plan(all_accounts, details_limit=details_limit) + delete_ids = dedupe_plan["delete_ids"] + deleted_ok = 0 + deleted_fail = 0 + failed_ids: List[int] = [] + if not dry_run and delete_ids: + delete_result = self._delete_accounts_parallel(delete_ids, timeout=timeout, workers=12) + deleted_ok = int(delete_result.get("deleted_ok", 0)) + deleted_fail = int(delete_result.get("deleted_fail", 0)) + failed_ids = list(delete_result.get("failed_ids") or []) + + return { + "dry_run": dry_run, + "total": len(all_accounts), + "duplicate_groups": int(dedupe_plan["duplicate_groups"]), + "duplicate_accounts": int(dedupe_plan["duplicate_accounts"]), + "to_delete": len(delete_ids), + "deleted_ok": deleted_ok, + "deleted_fail": deleted_fail, + "failed_delete_ids": failed_ids[:200], + "groups_preview": dedupe_plan["groups_preview"], + "truncated_groups": int(dedupe_plan["truncated_groups"]), + } + + def probe_accounts(self, account_ids: List[int], timeout: int = 30) -> Dict[str, Any]: + ids = sorted({ + account_id + for account_id in ( + self._normalize_account_id(item) + for item in (account_ids or []) + ) + if account_id is not None + }) + if not ids: + return { + "requested": 0, + "refreshed_ok": 0, + "refreshed_fail": 0, + "recovered": 0, + "still_abnormal": 0, + "details": [], + } + + before_status = self._list_accounts_by_ids(ids, timeout=timeout) + refresh_result = self._refresh_accounts_parallel(ids, timeout=max(30, timeout), workers=8) + success_ids = set(refresh_result.get("success_ids") or []) + failed_ids = set(refresh_result.get("failed_ids") or []) + + if success_ids: + time.sleep(2) + after_status = self._list_accounts_by_ids(ids, timeout=timeout) + + recovered_ids: List[int] = [] + abnormal_after_ids: List[int] = [] + details: List[Dict[str, Any]] = [] + for account_id in ids: + before = str(before_status.get(account_id) or "unknown").strip().lower() + after = str(after_status.get(account_id) or before or "unknown").strip().lower() + if self._is_abnormal_status(before) and not self._is_abnormal_status(after): + recovered_ids.append(account_id) + if self._is_abnormal_status(after): + abnormal_after_ids.append(account_id) + if len(details) < 200: + details.append({ + "id": account_id, + "before_status": before, + "after_status": after, + "refresh_ok": account_id in success_ids, + }) + + return { + "requested": len(ids), + "refreshed_ok": len(success_ids), + "refreshed_fail": len(failed_ids), + "recovered": len(recovered_ids), + "still_abnormal": len(abnormal_after_ids), + "details": details, + } + + def delete_accounts_batch(self, account_ids: List[int], timeout: int = 15) -> Dict[str, Any]: + ids = [ + account_id + for account_id in ( + self._normalize_account_id(item) + for item in (account_ids or []) + ) + if account_id is not None + ] + delete_result = self._delete_accounts_parallel(ids, timeout=timeout, workers=12) + return { + "requested": len({*ids}), + "deleted_ok": int(delete_result.get("deleted_ok", 0)), + "deleted_fail": int(delete_result.get("deleted_fail", 0)), + "deleted_ok_ids": list(delete_result.get("deleted_ok_ids") or []), + "failed_ids": list(delete_result.get("failed_ids") or []), + } + + def handle_exception_accounts( + self, + account_ids: Optional[List[int]] = None, + timeout: int = 30, + delete_unresolved: bool = True, + ) -> Dict[str, Any]: + requested_ids = [ + account_id + for account_id in ( + self._normalize_account_id(item) + for item in (account_ids or []) + ) + if account_id is not None + ] + + if requested_ids: + current_status = self._list_accounts_by_ids(requested_ids, timeout=timeout) + target_ids = [ + account_id + for account_id in requested_ids + if self._is_abnormal_status(current_status.get(account_id)) + ] + skipped_non_abnormal = max(0, len(set(requested_ids)) - len(target_ids)) + else: + all_accounts = self._list_all_accounts(timeout=timeout, page_size=100) + target_ids = [ + account_id + for account_id in ( + self._normalize_account_id(item.get("id")) + for item in all_accounts + if self._is_abnormal_status(item.get("status")) + ) + if account_id is not None + ] + skipped_non_abnormal = 0 + + unique_target_ids = sorted(set(target_ids)) + if not unique_target_ids: + return { + "requested": len(set(requested_ids)) if requested_ids else 0, + "targeted": 0, + "refreshed_ok": 0, + "refreshed_fail": 0, + "recovered": 0, + "remaining_abnormal": 0, + "deleted_ok": 0, + "deleted_fail": 0, + "skipped_non_abnormal": skipped_non_abnormal, + } + + refresh_result = self._refresh_accounts_parallel(unique_target_ids, timeout=max(30, timeout), workers=8) + if refresh_result.get("success_ids"): + time.sleep(2) + after_status = self._list_accounts_by_ids(unique_target_ids, timeout=timeout) + remaining_abnormal_ids = [ + account_id + for account_id in unique_target_ids + if self._is_abnormal_status(after_status.get(account_id)) + ] + remaining_abnormal_set = set(remaining_abnormal_ids) + recovered_ids = [ + account_id + for account_id in unique_target_ids + if account_id not in remaining_abnormal_set + ] + + delete_result = { + "deleted_ok": 0, + "deleted_fail": 0, + "deleted_ok_ids": [], + "failed_ids": [], + } + if delete_unresolved and remaining_abnormal_ids: + delete_result = self._delete_accounts_parallel(remaining_abnormal_ids, timeout=timeout, workers=12) + + return { + "requested": len(set(requested_ids)) if requested_ids else len(unique_target_ids), + "targeted": len(unique_target_ids), + "refreshed_ok": len(refresh_result.get("success_ids") or []), + "refreshed_fail": len(refresh_result.get("failed_ids") or []), + "recovered": len(recovered_ids), + "remaining_abnormal": len(remaining_abnormal_ids), + "deleted_ok": int(delete_result.get("deleted_ok", 0)), + "deleted_fail": int(delete_result.get("deleted_fail", 0)), + "deleted_ok_ids": list(delete_result.get("deleted_ok_ids") or []), + "failed_ids": list(delete_result.get("failed_ids") or []), + "skipped_non_abnormal": skipped_non_abnormal, + } + + def refresh_account(self, account_id: int, timeout: int = 30) -> bool: + try: + resp = self._request( + "POST", f"/api/v1/admin/accounts/{account_id}/refresh", + timeout=timeout, + ) + return resp.status_code in (200, 201) + except Exception: + return False + + def delete_account(self, account_id: int, timeout: int = 15) -> bool: + try: + resp = self._request( + "DELETE", f"/api/v1/admin/accounts/{account_id}", + timeout=timeout, + ) + return resp.status_code in (200, 204) + except Exception: + return False + + def get_pool_status(self, timeout: int = 15) -> Dict[str, Any]: + try: + all_accounts = self._list_all_accounts(timeout=timeout, page_size=100) + error = sum( + 1 for account in all_accounts + if self._is_abnormal_status(account.get("status")) + ) + total = len(all_accounts) + normal = max(0, total - error) + return { + "total": total, + "candidates": normal, + "error_count": error, + "threshold": self.min_candidates, + "healthy": normal >= self.min_candidates, + "percent": round(normal / self.min_candidates * 100, 1) if self.min_candidates > 0 else 100, + "last_checked": time.strftime("%Y-%m-%d %H:%M:%S"), + "error": None, + } + except Exception as e: + return { + "total": 0, "candidates": 0, "error_count": 0, + "threshold": self.min_candidates, "healthy": False, + "percent": 0, "last_checked": time.strftime("%Y-%m-%d %H:%M:%S"), + "error": str(e), + } + + def test_connection(self, timeout: int = 10) -> Dict[str, Any]: + try: + status = self.get_pool_status(timeout) + total = int(status.get("total", 0)) + normal = int(status.get("candidates", 0)) + error = int(status.get("error_count", 0)) + return { + "ok": True, + "total": total, + "normal": normal, + "error": error, + "message": f"连接成功,共 {total} 个账号,{normal} 正常,{error} 异常", + } + except Exception as e: + return {"ok": False, "total": 0, "normal": 0, "error": 0, + "message": f"连接失败: {e}"} + + def _list_accounts_by_ids( + self, ids: List[int], timeout: int = 15, + ) -> Dict[int, str]: + """查询指定 ID 的账号当前状态,返回 {id: status}""" + result: Dict[int, str] = {} + id_set = set(ids) + page = 1 + while id_set: + data = self.list_accounts(page=page, page_size=100, timeout=timeout) + items = data.get("items") or [] + if not items: + break + for item in items: + aid = item.get("id") + if aid in id_set: + result[aid] = str(item.get("status", "")) + id_set.discard(aid) + total = data.get("total", 0) + if page * 100 >= total or len(items) < 100: + break + page += 1 + return result + + def probe_and_clean_sync(self, timeout: int = 15, actions: Optional[Dict[str, bool]] = None) -> Dict[str, Any]: + action_flags = { + "refresh_abnormal_accounts": bool((actions or {}).get("refresh_abnormal_accounts", True)), + "delete_abnormal_accounts": bool((actions or {}).get("delete_abnormal_accounts", True)), + "dedupe_duplicate_accounts": bool((actions or {}).get("dedupe_duplicate_accounts", True)), + } + started = time.time() + all_accounts = self._list_all_accounts(timeout=timeout, page_size=100) + + error_accounts = [ + account for account in all_accounts + if self._is_abnormal_status(account.get("status")) + ] + + error_ids = [ + self._normalize_account_id(acc.get("id")) + for acc in error_accounts + ] + error_ids = [i for i in error_ids if i is not None] + initial_error_ids = set(error_ids) + + refresh_result = {"success_ids": [], "failed_ids": []} + if action_flags["refresh_abnormal_accounts"] and error_ids: + refresh_result = self._refresh_accounts_parallel(error_ids, timeout=30, workers=8) + + refreshed_ids = list(refresh_result.get("success_ids") or []) + refresh_failed_ids = list(refresh_result.get("failed_ids") or []) + + current_accounts = all_accounts + current_error_ids = set(initial_error_ids) + if refreshed_ids: + time.sleep(2) + if action_flags["refresh_abnormal_accounts"] and (error_ids or refreshed_ids): + current_accounts = self._list_all_accounts(timeout=timeout, page_size=100) + current_error_ids = { + int(acc_id) for acc_id in ( + self._normalize_account_id(account.get("id")) + for account in current_accounts + if self._is_abnormal_status(account.get("status")) + ) if isinstance(acc_id, int) + } + recovered = len(initial_error_ids - current_error_ids) + + dedupe_plan = { + "duplicate_groups": 0, + "duplicate_accounts": 0, + "delete_ids": [], + "groups_preview": [], + "truncated_groups": 0, + } + duplicate_delete_ids: List[int] = [] + if action_flags["dedupe_duplicate_accounts"]: + dedupe_plan = self._build_dedupe_plan(current_accounts, details_limit=120) + duplicate_delete_ids = [int(i) for i in dedupe_plan["delete_ids"] if isinstance(i, int)] + normal_count = len(current_accounts) - len(current_error_ids) + + delete_targets: set[int] = set() + if action_flags["delete_abnormal_accounts"]: + delete_targets.update(current_error_ids) + if action_flags["dedupe_duplicate_accounts"]: + delete_targets.update(duplicate_delete_ids) + delete_result = self._delete_accounts_parallel(sorted(delete_targets, reverse=True), timeout=timeout, workers=12) + deleted_ok = int(delete_result.get("deleted_ok", 0)) + deleted_fail = int(delete_result.get("deleted_fail", 0)) + deleted_ok_ids = set(int(i) for i in (delete_result.get("deleted_ok_ids") or []) if isinstance(i, int)) + + deleted_from_error = len(deleted_ok_ids & set(current_error_ids)) + deleted_from_duplicate = len(deleted_ok_ids & set(duplicate_delete_ids)) + + elapsed_ms = int((time.time() - started) * 1000) + + return { + "actions": action_flags, + "total": len(current_accounts), "normal": normal_count, + "initial_error_count": len(initial_error_ids), + "error_count": len(current_error_ids), "refreshed": recovered, + "refresh_attempted": len(error_ids) if action_flags["refresh_abnormal_accounts"] else 0, + "refresh_failed": len(refresh_failed_ids), + "deleted_ok": deleted_ok, "deleted_fail": deleted_fail, + "duplicate_groups": int(dedupe_plan["duplicate_groups"]), + "duplicate_accounts": int(dedupe_plan["duplicate_accounts"]), + "duplicate_to_delete": len(duplicate_delete_ids), + "deleted_from_error": deleted_from_error, + "deleted_from_duplicate": deleted_from_duplicate, + "duration_ms": elapsed_ms, + } + + def calculate_gap(self, current_candidates: Optional[int] = None) -> int: + if current_candidates is None: + status = self.get_pool_status() + if status.get("error"): + raise RuntimeError(f"Sub2Api 池状态查询失败: {status['error']}") + current_candidates = status["candidates"] + return max(0, self.min_candidates - current_candidates) diff --git a/openai_pool_orchestrator/register.py b/openai_pool_orchestrator/register.py new file mode 100755 index 0000000..89a8e8b --- /dev/null +++ b/openai_pool_orchestrator/register.py @@ -0,0 +1,2120 @@ +import json +import os +import re +import sys +import time +import uuid +import math +import random +import string +import secrets +import socket +import hashlib +import base64 +import threading +import argparse +import queue +import tempfile +from http.cookies import SimpleCookie +from datetime import datetime, timezone, timedelta +from urllib.parse import urlparse, parse_qs, urlencode, quote +from dataclasses import dataclass +from typing import Any, Dict, Optional, Callable +import urllib.parse +import urllib.request +import urllib.error + +from curl_cffi import requests + +# ========================================== +# 日志事件发射器 +# ========================================== + + +class EventEmitter: + """ + 将注册流程中的日志事件发射到队列,供 SSE 消费。 + 同时支持 CLI 模式(直接 print)。 + """ + + def __init__( + self, + q: Optional[queue.Queue] = None, + cli_mode: bool = False, + defaults: Optional[Dict[str, Any]] = None, + ): + self._q = q + self._cli_mode = cli_mode + self._defaults = dict(defaults or {}) + + def emit(self, level: str, message: str, step: str = "", **extra: Any) -> None: + """ + level: "info" | "success" | "error" | "warn" + step: 可选的流程阶段标识,如 "check_proxy" / "create_email" 等 + """ + ts = datetime.now().strftime("%H:%M:%S") + event = { + "ts": ts, + "level": level, + "message": message, + "step": step, + } + if self._defaults: + event.update(self._defaults) + if extra: + event.update({k: v for k, v in extra.items() if v is not None}) + if self._cli_mode: + prefix_map = { + "info": "[*]", + "success": "[+]", + "error": "[Error]", + "warn": "[!]", + } + prefix = prefix_map.get(level, "[*]") + print(f"{prefix} {message}") + if self._q is not None: + try: + self._q.put_nowait(event) + except queue.Full: + pass + + def bind(self, **defaults: Any) -> "EventEmitter": + merged = dict(self._defaults) + merged.update({k: v for k, v in defaults.items() if v is not None}) + return EventEmitter(q=self._q, cli_mode=self._cli_mode, defaults=merged) + + def info(self, msg: str, step: str = "", **extra: Any) -> None: + self.emit("info", msg, step, **extra) + + def success(self, msg: str, step: str = "", **extra: Any) -> None: + self.emit("success", msg, step, **extra) + + def error(self, msg: str, step: str = "", **extra: Any) -> None: + self.emit("error", msg, step, **extra) + + def warn(self, msg: str, step: str = "", **extra: Any) -> None: + self.emit("warn", msg, step, **extra) + + +# 默认 CLI 发射器(兼容直接运行) +_cli_emitter = EventEmitter(cli_mode=True) + + +# ========================================== +# Mail.tm 临时邮箱 API +# ========================================== + +MAILTM_BASE = "https://api.mail.tm" +DEFAULT_PROXY_POOL_URL = "https://zenproxy.top/api/fetch" +DEFAULT_PROXY_POOL_AUTH_MODE = "query" +DEFAULT_PROXY_POOL_API_KEY = "19c0ec43-8f76-4c97-81bc-bcda059eeba4" +DEFAULT_PROXY_POOL_COUNT = 1 +DEFAULT_PROXY_POOL_COUNTRY = "US" +DEFAULT_HTTP_VERSION = "v2" +H3_PROXY_ERROR_HINT = "HTTP/3 is not supported over an HTTP proxy" +TRANSIENT_TLS_ERROR_HINTS = ( + "curl: (35)", + "TLS connect error", + "OPENSSL_internal:invalid library", + "SSL_ERROR_SYSCALL", +) +TRANSIENT_TLS_RETRY_COUNT = 2 +POOL_RELAY_RETRIES = 2 +POOL_PROXY_FETCH_RETRIES = 3 +POOL_RELAY_REQUEST_RETRIES = 2 + + +def _is_transient_tls_error(exc: Exception | str) -> bool: + message = str(exc or "") + return any(hint in message for hint in TRANSIENT_TLS_ERROR_HINTS) + + +def _call_with_http_fallback(request_func, url: str, **kwargs: Any): + """ + curl_cffi 在某些站点可能优先尝试 H3,遇到 HTTP 代理不支持时自动降级到 HTTP/1.1 重试。 + 对 curl TLS 握手异常(如 curl: (35))也进行有限重试,并优先降级到 HTTP/1.1。 + """ + try: + return request_func(url, **kwargs) + except Exception as exc: + message = str(exc) + if H3_PROXY_ERROR_HINT in message: + retry_kwargs = dict(kwargs) + retry_kwargs["http_version"] = "v1" + return request_func(url, **retry_kwargs) + if not _is_transient_tls_error(message): + raise + + last_exc: Exception = exc + candidate_kwargs_list = [dict(kwargs)] + if str(kwargs.get("http_version") or "").strip().lower() != "v1": + retry_kwargs = dict(kwargs) + retry_kwargs["http_version"] = "v1" + candidate_kwargs_list.append(retry_kwargs) + + for candidate_kwargs in candidate_kwargs_list: + for attempt in range(TRANSIENT_TLS_RETRY_COUNT): + time.sleep(min(0.35 * (attempt + 1), 1.0)) + try: + return request_func(url, **candidate_kwargs) + except Exception as retry_exc: + last_exc = retry_exc + retry_message = str(retry_exc) + if H3_PROXY_ERROR_HINT in retry_message and str(candidate_kwargs.get("http_version") or "").strip().lower() != "v1": + candidate_kwargs = dict(candidate_kwargs) + candidate_kwargs["http_version"] = "v1" + continue + if not _is_transient_tls_error(retry_message): + raise + raise last_exc + +def _normalize_proxy_value(proxy_value: Any) -> str: + value = str(proxy_value or "").strip().strip('"').strip("'") + if not value: + return "" + if value.startswith("{") or value.startswith("[") or value.startswith("<"): + return "" + if "://" in value: + return value + if ":" not in value: + return "" + return f"http://{value}" + + +def _to_proxies_dict(proxy_value: str) -> Optional[Dict[str, str]]: + normalized = _normalize_proxy_value(proxy_value) + if not normalized: + return None + return {"http": normalized, "https": normalized} + + +def _build_proxy_from_host_port(host: Any, port: Any, proxy_type: Any = "") -> str: + host_value = str(host or "").strip() + port_value = str(port or "").strip() + if not host_value or not port_value: + return "" + proxy_type_value = str(proxy_type or "").strip().lower() + if proxy_type_value in ("socks5", "socks", "shadowsocks"): + return _normalize_proxy_value(f"socks5://{host_value}:{port_value}") + return _normalize_proxy_value(f"http://{host_value}:{port_value}") + + +def _pool_host_from_api_url(api_url: str) -> str: + raw = str(api_url or "").strip() + if not raw: + return "" + if "://" not in raw: + raw = "https://" + raw + try: + parsed = urlparse(raw) + return str(parsed.hostname or "").strip() + except Exception: + return "" + + +def _pool_relay_url_from_fetch_url(api_url: str) -> str: + raw = str(api_url or "").strip() + if not raw: + return "" + if "://" not in raw: + raw = "https://" + raw + try: + parsed = urlparse(raw) + scheme = parsed.scheme or "https" + netloc = parsed.netloc + if not netloc: + return "" + return f"{scheme}://{netloc}/api/relay" + except Exception: + return "" + + +def _trace_via_pool_relay(pool_cfg: Dict[str, Any]) -> str: + relay_url = _pool_relay_url_from_fetch_url(str(pool_cfg.get("api_url") or "")) + if not relay_url: + raise RuntimeError("代理池 relay 地址解析失败") + + api_key = str(pool_cfg.get("api_key") or DEFAULT_PROXY_POOL_API_KEY).strip() or DEFAULT_PROXY_POOL_API_KEY + country = str(pool_cfg.get("country") or DEFAULT_PROXY_POOL_COUNTRY).strip().upper() or DEFAULT_PROXY_POOL_COUNTRY + timeout = int(pool_cfg.get("timeout_seconds") or 10) + timeout = max(8, min(timeout, 30)) + + params = { + "api_key": api_key, + "url": "https://cloudflare.com/cdn-cgi/trace", + "country": country, + } + retry_count = max(1, int(pool_cfg.get("relay_retries") or POOL_RELAY_RETRIES)) + last_error = "" + for i in range(retry_count): + try: + resp = _call_with_http_fallback( + requests.get, + relay_url, + params=params, + impersonate="chrome", + timeout=timeout, + ) + if resp.status_code == 200: + return str(resp.text or "") + last_error = f"HTTP {resp.status_code}" + except Exception as exc: + last_error = str(exc) + if i < retry_count - 1: + time.sleep(min(0.3 * (i + 1), 1.0)) + raise RuntimeError(f"代理池 relay 请求失败: {last_error or 'unknown error'}") +def _extract_proxy_from_obj(obj: Any, relay_host: str = "") -> str: + if isinstance(obj, str): + return _normalize_proxy_value(obj) + if isinstance(obj, (list, tuple)): + for item in obj: + proxy = _extract_proxy_from_obj(item, relay_host) + if proxy: + return proxy + return "" + if isinstance(obj, dict): + local_port = obj.get("local_port") + if local_port in (None, ""): + local_port = obj.get("localPort") + if local_port not in (None, ""): + # ZenProxy 文档中的 local_port 是代理绑定端口,优先使用 api_url 主机名。 + if relay_host: + proxy = _normalize_proxy_value(f"http://{relay_host}:{local_port}") + if proxy: + return proxy + proxy = _normalize_proxy_value(f"http://127.0.0.1:{local_port}") + if proxy: + return proxy + + host = str(obj.get("ip") or obj.get("host") or obj.get("server") or "").strip() + port = str(obj.get("port") or "").strip() + proxy_type = obj.get("type") or obj.get("protocol") or obj.get("scheme") or "" + if host and port: + proxy = _build_proxy_from_host_port(host, port, proxy_type) + if proxy: + return proxy + + for key in ("proxy", "proxy_url", "url", "value", "result", "data", "proxy_list", "list", "proxies"): + if key in obj: + proxy = _extract_proxy_from_obj(obj.get(key), relay_host) + if proxy: + return proxy + + for value in obj.values(): + proxy = _extract_proxy_from_obj(value, relay_host) + if proxy: + return proxy + return "" + + +def _proxy_tcp_reachable(proxy_url: str, timeout_seconds: float = 1.2) -> bool: + value = str(proxy_url or "").strip() + if not value: + return False + if "://" not in value: + value = "http://" + value + try: + parsed = urlparse(value) + host = str(parsed.hostname or "").strip() + port = int(parsed.port or 0) + except Exception: + return False + if not host or port <= 0: + return False + try: + with socket.create_connection((host, port), timeout=timeout_seconds): + return True + except Exception: + return False + + +def _fetch_proxy_from_pool(pool_cfg: Dict[str, Any]) -> str: + enabled = bool(pool_cfg.get("enabled")) + if not enabled: + return "" + + api_url = str(pool_cfg.get("api_url") or DEFAULT_PROXY_POOL_URL).strip() or DEFAULT_PROXY_POOL_URL + auth_mode = str(pool_cfg.get("auth_mode") or DEFAULT_PROXY_POOL_AUTH_MODE).strip().lower() + if auth_mode not in ("header", "query"): + auth_mode = DEFAULT_PROXY_POOL_AUTH_MODE + api_key = str(pool_cfg.get("api_key") or DEFAULT_PROXY_POOL_API_KEY).strip() or DEFAULT_PROXY_POOL_API_KEY + relay_host = str(pool_cfg.get("relay_host") or "").strip() + if not relay_host: + relay_host = _pool_host_from_api_url(api_url) + try: + count = int(pool_cfg.get("count") or DEFAULT_PROXY_POOL_COUNT) + except (TypeError, ValueError): + count = DEFAULT_PROXY_POOL_COUNT + count = max(1, min(count, 20)) + country = str(pool_cfg.get("country") or DEFAULT_PROXY_POOL_COUNTRY).strip().upper() or DEFAULT_PROXY_POOL_COUNTRY + timeout = int(pool_cfg.get("timeout_seconds") or 10) + timeout = max(3, min(timeout, 30)) + + headers: Dict[str, str] = {} + params: Dict[str, str] = {"count": str(count), "country": country} + if auth_mode == "query": + params["api_key"] = api_key + else: + headers["Authorization"] = f"Bearer {api_key}" + + resp = _call_with_http_fallback( + requests.get, + api_url, + headers=headers or None, + params=params or None, + http_version=DEFAULT_HTTP_VERSION, + impersonate="chrome", + timeout=timeout, + ) + if resp.status_code != 200: + raise RuntimeError(f"代理池请求失败: HTTP {resp.status_code}") + + proxy = "" + try: + payload = resp.json() + if isinstance(payload, dict): + proxies = payload.get("proxies") + if isinstance(proxies, list): + for item in proxies: + proxy = _extract_proxy_from_obj(item, relay_host) + if proxy: + break + if not proxy: + proxy = _extract_proxy_from_obj(payload, relay_host) + except Exception: + proxy = "" + + if not proxy: + proxy = _normalize_proxy_value(resp.text) + if not proxy: + raise RuntimeError("代理池响应中未找到可用代理") + return proxy + + +def _resolve_request_proxies( + default_proxies: Any = None, + proxy_selector: Optional[Callable[[], Any]] = None, +) -> Any: + if not proxy_selector: + return default_proxies + try: + selected = proxy_selector() + if selected is not None: + return selected + except Exception: + pass + return default_proxies + + +def _mailtm_headers(*, token: str = "", use_json: bool = False) -> Dict[str, str]: + headers = {"Accept": "application/json"} + if use_json: + headers["Content-Type"] = "application/json" + if token: + headers["Authorization"] = f"Bearer {token}" + return headers + + +def _mailtm_domains(proxies: Any = None) -> list[str]: + resp = _call_with_http_fallback( + requests.get, + f"{MAILTM_BASE}/domains", + headers=_mailtm_headers(), + proxies=proxies, + http_version=DEFAULT_HTTP_VERSION, + impersonate="chrome", + timeout=15, + ) + if resp.status_code != 200: + raise RuntimeError(f"获取 Mail.tm 域名失败,状态码: {resp.status_code}") + + data = resp.json() + domains = [] + if isinstance(data, list): + items = data + elif isinstance(data, dict): + items = data.get("hydra:member") or data.get("items") or [] + else: + items = [] + + for item in items: + if not isinstance(item, dict): + continue + domain = str(item.get("domain") or "").strip() + is_active = item.get("isActive", True) + is_private = item.get("isPrivate", False) + if domain and is_active and not is_private: + domains.append(domain) + + return domains + + +def get_email_and_token( + proxies: Any = None, + emitter: EventEmitter = _cli_emitter, + proxy_selector: Optional[Callable[[], Any]] = None, +) -> tuple[str, str]: + """创建 Mail.tm 邮箱并获取 Bearer Token""" + try: + domains = _mailtm_domains(_resolve_request_proxies(proxies, proxy_selector)) + if not domains: + emitter.error("Mail.tm 没有可用域名", step="create_email") + return "", "" + domain = random.choice(domains) + + for _ in range(5): + local = f"oc{secrets.token_hex(5)}" + email = f"{local}@{domain}" + password = secrets.token_urlsafe(18) + + create_resp = _call_with_http_fallback( + requests.post, + f"{MAILTM_BASE}/accounts", + headers=_mailtm_headers(use_json=True), + json={"address": email, "password": password}, + proxies=_resolve_request_proxies(proxies, proxy_selector), + http_version=DEFAULT_HTTP_VERSION, + impersonate="chrome", + timeout=15, + ) + + if create_resp.status_code not in (200, 201): + continue + + token_resp = _call_with_http_fallback( + requests.post, + f"{MAILTM_BASE}/token", + headers=_mailtm_headers(use_json=True), + json={"address": email, "password": password}, + proxies=_resolve_request_proxies(proxies, proxy_selector), + http_version=DEFAULT_HTTP_VERSION, + impersonate="chrome", + timeout=15, + ) + + if token_resp.status_code == 200: + token = str(token_resp.json().get("token") or "").strip() + if token: + return email, token + + emitter.error("Mail.tm 邮箱创建成功但获取 Token 失败", step="create_email") + return "", "" + except Exception as e: + emitter.error(f"请求 Mail.tm API 出错: {e}", step="create_email") + return "", "" + + +def get_oai_code( + token: str, email: str, proxies: Any = None, emitter: EventEmitter = _cli_emitter, + stop_event: Optional[threading.Event] = None, + proxy_selector: Optional[Callable[[], Any]] = None, +) -> str: + """使用 Mail.tm Token 轮询获取 OpenAI 验证码""" + url_list = f"{MAILTM_BASE}/messages" + regex = r"(? str: + return base64.urlsafe_b64encode(raw).decode("ascii").rstrip("=") + + +def _sha256_b64url_no_pad(s: str) -> str: + return _b64url_no_pad(hashlib.sha256(s.encode("ascii")).digest()) + + +def _random_state(nbytes: int = 16) -> str: + return secrets.token_urlsafe(nbytes) + + +def _pkce_verifier() -> str: + return secrets.token_urlsafe(64) + + +def _parse_callback_url(callback_url: str) -> Dict[str, str]: + candidate = callback_url.strip() + if not candidate: + return {"code": "", "state": "", "error": "", "error_description": ""} + + if "://" not in candidate: + if candidate.startswith("?"): + candidate = f"http://localhost{candidate}" + elif any(ch in candidate for ch in "/?#") or ":" in candidate: + candidate = f"http://{candidate}" + elif "=" in candidate: + candidate = f"http://localhost/?{candidate}" + + parsed = urllib.parse.urlparse(candidate) + query = urllib.parse.parse_qs(parsed.query, keep_blank_values=True) + fragment = urllib.parse.parse_qs(parsed.fragment, keep_blank_values=True) + + for key, values in fragment.items(): + if key not in query or not query[key] or not (query[key][0] or "").strip(): + query[key] = values + + def get1(k: str) -> str: + v = query.get(k, [""]) + return (v[0] or "").strip() + + code = get1("code") + state = get1("state") + error = get1("error") + error_description = get1("error_description") + + if code and not state and "#" in code: + code, state = code.split("#", 1) + + if not error and error_description: + error, error_description = error_description, "" + + return { + "code": code, + "state": state, + "error": error, + "error_description": error_description, + } + + +def _jwt_claims_no_verify(id_token: str) -> Dict[str, Any]: + if not id_token or id_token.count(".") < 2: + return {} + payload_b64 = id_token.split(".")[1] + pad = "=" * ((4 - (len(payload_b64) % 4)) % 4) + try: + payload = base64.urlsafe_b64decode((payload_b64 + pad).encode("ascii")) + return json.loads(payload.decode("utf-8")) + except Exception: + return {} + + +def _decode_jwt_segment(seg: str) -> Dict[str, Any]: + raw = (seg or "").strip() + if not raw: + return {} + pad = "=" * ((4 - (len(raw) % 4)) % 4) + try: + decoded = base64.urlsafe_b64decode((raw + pad).encode("ascii")) + return json.loads(decoded.decode("utf-8")) + except Exception: + return {} + + +def _to_int(v: Any) -> int: + try: + return int(v) + except (TypeError, ValueError): + return 0 + + +def _post_form( + url: str, + data: Dict[str, str], + timeout: int = 30, + proxy: str = "", +) -> Dict[str, Any]: + body = urllib.parse.urlencode(data).encode("utf-8") + req = urllib.request.Request( + url, + data=body, + method="POST", + headers={ + "Content-Type": "application/x-www-form-urlencoded", + "Accept": "application/json", + }, + ) + handlers = [] + normalized_proxy = _normalize_proxy_value(proxy) + if normalized_proxy: + handlers.append(urllib.request.ProxyHandler({"http": normalized_proxy, "https": normalized_proxy})) + opener = urllib.request.build_opener(*handlers) + try: + with opener.open(req, timeout=timeout) as resp: + raw = resp.read() + if resp.status != 200: + raise RuntimeError( + f"token exchange failed: {resp.status}: {raw.decode('utf-8', 'replace')}" + ) + return json.loads(raw.decode("utf-8")) + except urllib.error.HTTPError as exc: + raw = exc.read() + raise RuntimeError( + f"token exchange failed: {exc.code}: {raw.decode('utf-8', 'replace')}" + ) from exc + + +def _build_token_result(token_payload: Dict[str, Any], account_password: str = "") -> str: + access_token = str(token_payload.get("access_token") or "").strip() + refresh_token = str(token_payload.get("refresh_token") or "").strip() + id_token = str(token_payload.get("id_token") or "").strip() + expires_in = _to_int(token_payload.get("expires_in")) + + missing_fields = [ + name for name, value in ( + ("access_token", access_token), + ("refresh_token", refresh_token), + ("id_token", id_token), + ) if not value + ] + if missing_fields: + raise ValueError(f"token exchange missing fields: {', '.join(missing_fields)}") + + claims = _jwt_claims_no_verify(id_token) + email = str(claims.get("email") or "").strip() + auth_claims = claims.get("https://api.openai.com/auth") or {} + account_id = str(auth_claims.get("chatgpt_account_id") or "").strip() + if not email or not account_id: + raise ValueError("token exchange missing email/account_id in id_token") + + now = int(time.time()) + expired_rfc3339 = time.strftime( + "%Y-%m-%dT%H:%M:%SZ", time.gmtime(now + max(expires_in, 0)) + ) + now_rfc3339 = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(now)) + + config = { + "id_token": id_token, + "access_token": access_token, + "refresh_token": refresh_token, + "account_id": account_id, + "last_refresh": now_rfc3339, + "expires_at": expired_rfc3339, + "email": email, + "type": "codex", + "expired": expired_rfc3339, + } + if account_password: + config["account_password"] = account_password + return json.dumps(config, ensure_ascii=False, separators=(",", ":")) + + +def _write_text_atomic(file_path: str, content: str) -> None: + directory = os.path.dirname(file_path) or "." + os.makedirs(directory, exist_ok=True) + fd, tmp_path = tempfile.mkstemp(prefix=".tmp_", suffix=".json", dir=directory) + try: + with os.fdopen(fd, "w", encoding="utf-8") as handle: + handle.write(content) + handle.flush() + os.fsync(handle.fileno()) + os.replace(tmp_path, file_path) + finally: + try: + if os.path.exists(tmp_path): + os.remove(tmp_path) + except OSError: + pass + + +@dataclass(frozen=True) +class OAuthStart: + auth_url: str + state: str + code_verifier: str + redirect_uri: str + + +def generate_oauth_url( + *, redirect_uri: str = DEFAULT_REDIRECT_URI, scope: str = DEFAULT_SCOPE +) -> OAuthStart: + state = _random_state() + code_verifier = _pkce_verifier() + code_challenge = _sha256_b64url_no_pad(code_verifier) + + params = { + "client_id": CLIENT_ID, + "response_type": "code", + "redirect_uri": redirect_uri, + "scope": scope, + "state": state, + "code_challenge": code_challenge, + "code_challenge_method": "S256", + "prompt": "login", + "id_token_add_organizations": "true", + "codex_cli_simplified_flow": "true", + } + auth_url = f"{AUTH_URL}?{urllib.parse.urlencode(params)}" + return OAuthStart( + auth_url=auth_url, + state=state, + code_verifier=code_verifier, + redirect_uri=redirect_uri, + ) + + +def submit_callback_url( + *, + callback_url: str, + expected_state: str, + code_verifier: str, + redirect_uri: str = DEFAULT_REDIRECT_URI, + proxy: str = "", +) -> str: + cb = _parse_callback_url(callback_url) + if cb["error"]: + desc = cb["error_description"] + raise RuntimeError(f"oauth error: {cb['error']}: {desc}".strip()) + + if not cb["code"]: + raise ValueError("callback url missing ?code=") + if not cb["state"]: + raise ValueError("callback url missing ?state=") + if cb["state"] != expected_state: + raise ValueError("state mismatch") + + token_resp = _post_form( + TOKEN_URL, + { + "grant_type": "authorization_code", + "client_id": CLIENT_ID, + "code": cb["code"], + "redirect_uri": redirect_uri, + "code_verifier": code_verifier, + }, + proxy=proxy, + ) + + return _build_token_result(token_resp) + + +# ========================================== +# 核心注册逻辑 +# ========================================== + +from . import TOKENS_DIR as _PKG_TOKENS_DIR + +TOKENS_DIR = str(_PKG_TOKENS_DIR) + + +def run( + proxy: Optional[str], + emitter: EventEmitter = _cli_emitter, + stop_event: Optional[threading.Event] = None, + mail_provider=None, + proxy_pool_config: Optional[Dict[str, Any]] = None, +) -> Optional[str]: + static_proxy = _normalize_proxy_value(proxy) + static_proxies: Any = _to_proxies_dict(static_proxy) + + pool_cfg_raw = proxy_pool_config or {} + pool_cfg = { + "enabled": bool(pool_cfg_raw.get("enabled", False)), + "api_url": str(pool_cfg_raw.get("api_url") or DEFAULT_PROXY_POOL_URL).strip() or DEFAULT_PROXY_POOL_URL, + "auth_mode": str(pool_cfg_raw.get("auth_mode") or DEFAULT_PROXY_POOL_AUTH_MODE).strip().lower() or DEFAULT_PROXY_POOL_AUTH_MODE, + "api_key": str(pool_cfg_raw.get("api_key") or DEFAULT_PROXY_POOL_API_KEY).strip() or DEFAULT_PROXY_POOL_API_KEY, + "count": pool_cfg_raw.get("count", DEFAULT_PROXY_POOL_COUNT), + "country": str(pool_cfg_raw.get("country") or DEFAULT_PROXY_POOL_COUNTRY).strip().upper() or DEFAULT_PROXY_POOL_COUNTRY, + "timeout_seconds": int(pool_cfg_raw.get("timeout_seconds") or 10), + } + if pool_cfg["auth_mode"] not in ("header", "query"): + pool_cfg["auth_mode"] = DEFAULT_PROXY_POOL_AUTH_MODE + try: + pool_cfg["count"] = max(1, min(int(pool_cfg.get("count") or DEFAULT_PROXY_POOL_COUNT), 20)) + except (TypeError, ValueError): + pool_cfg["count"] = DEFAULT_PROXY_POOL_COUNT + + last_pool_proxy = "" + pool_fail_streak = 0 + warned_fallback = False + + def _next_proxy_value() -> str: + nonlocal last_pool_proxy, pool_fail_streak, warned_fallback + if pool_cfg["enabled"]: + max_fetch_retries = max(1, int(pool_cfg.get("fetch_retries") or POOL_PROXY_FETCH_RETRIES)) + last_error = "" + for _ in range(max_fetch_retries): + try: + fetched = _fetch_proxy_from_pool(pool_cfg) + if fetched and not _proxy_tcp_reachable(fetched): + last_error = f"代理池代理不可达: {fetched}" + continue + last_pool_proxy = fetched + pool_fail_streak = 0 + warned_fallback = False + return fetched + except Exception as e: + last_error = str(e) + + pool_fail_streak += 1 + if static_proxy: + if not warned_fallback: + emitter.warn(f"代理池不可用,回退固定代理: {last_error or 'unknown error'}", step="check_proxy") + warned_fallback = True + return static_proxy + if pool_fail_streak <= 3: + emitter.warn(f"代理池不可用: {last_error or 'unknown error'}", step="check_proxy") + return "" + return static_proxy + def _next_proxies() -> Any: + proxy_value = _next_proxy_value() + return _to_proxies_dict(proxy_value) + + # 随机 Chrome 指纹,避免 OpenAI 反机器人检测 + _chrome_profiles = [ + {"major": 119, "imp": "chrome119", "build": 6045, "patch": (123, 200), + "sec": '"Google Chrome";v="119", "Chromium";v="119", "Not?A_Brand";v="24"'}, + {"major": 120, "imp": "chrome120", "build": 6099, "patch": (62, 200), + "sec": '"Not_A Brand";v="8", "Chromium";v="120", "Google Chrome";v="120"'}, + {"major": 123, "imp": "chrome123", "build": 6312, "patch": (46, 170), + "sec": '"Google Chrome";v="123", "Not:A-Brand";v="8", "Chromium";v="123"'}, + {"major": 124, "imp": "chrome124", "build": 6367, "patch": (60, 180), + "sec": '"Chromium";v="124", "Google Chrome";v="124", "Not-A.Brand";v="99"'}, + ] + _cp = random.choice(_chrome_profiles) + _chrome_full = f"{_cp['major']}.0.{_cp['build']}.{random.randint(*_cp['patch'])}" + _chrome_ua = f"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/{_chrome_full} Safari/537.36" + + s = requests.Session(impersonate=_cp["imp"]) + s.headers.update({ + "User-Agent": _chrome_ua, + "Accept-Language": random.choice(["en-US,en;q=0.9", "en-US,en;q=0.9,zh-CN;q=0.8", "en,en-US;q=0.9"]), + "sec-ch-ua": _cp["sec"], + "sec-ch-ua-mobile": "?0", + "sec-ch-ua-platform": '"Windows"', + "sec-ch-ua-arch": '"x86"', + "sec-ch-ua-bitness": '"64"', + "sec-ch-ua-full-version": f'"{_chrome_full}"', + "sec-ch-ua-platform-version": f'"{random.randint(10, 15)}.0.0"', + }) + + def _trace_headers() -> Dict[str, str]: + """生成 DataDog trace headers,模拟真实浏览器监控""" + trace_id = random.randint(10**17, 10**18 - 1) + parent_id = random.randint(10**17, 10**18 - 1) + tp = f"00-{uuid.uuid4().hex}-{format(parent_id, '016x')}-01" + return { + "traceparent": tp, "tracestate": "dd=s:1;o:rum", + "x-datadog-origin": "rum", "x-datadog-sampling-priority": "1", + "x-datadog-trace-id": str(trace_id), "x-datadog-parent-id": str(parent_id), + } + pool_relay_url = _pool_relay_url_from_fetch_url(str(pool_cfg.get("api_url") or "")) + pool_relay_enabled = bool(pool_cfg["enabled"] and pool_relay_url) + relay_cookie_jar: Dict[str, str] = {} + pool_relay_api_key = str(pool_cfg.get("api_key") or DEFAULT_PROXY_POOL_API_KEY).strip() or DEFAULT_PROXY_POOL_API_KEY + pool_relay_country = str(pool_cfg.get("country") or DEFAULT_PROXY_POOL_COUNTRY).strip().upper() or DEFAULT_PROXY_POOL_COUNTRY + relay_fallback_warned = False + relay_bypass_openai_hosts = False + openai_relay_probe_done = False + mail_proxy_selector = None if pool_relay_enabled else _next_proxy_value + mail_proxies_selector = None if pool_relay_enabled else _next_proxies + + def _fallback_proxies_for_relay_failure() -> Any: + if static_proxy: + return _to_proxies_dict(static_proxy) + return None + + def _target_host(target_url: str) -> str: + return str(urlparse(str(target_url or "")).hostname or "").strip().lower() + + def _is_openai_like_host(host: str) -> bool: + return bool(host) and (host.endswith("openai.com") or host.endswith("chatgpt.com")) + + def _should_bypass_relay_for_target(target_url: str) -> bool: + host = _target_host(target_url) + return relay_bypass_openai_hosts and _is_openai_like_host(host) + + def _warn_relay_fallback(reason: str, target_url: str) -> None: + nonlocal relay_fallback_warned, relay_bypass_openai_hosts + host = _target_host(target_url) or str(target_url or "?") + if _is_openai_like_host(host): + relay_bypass_openai_hosts = True + if relay_fallback_warned: + return + if static_proxy: + emitter.warn(f"代理池 relay 对 {host} 不可用,回退固定代理: {reason}", step="check_proxy") + else: + emitter.warn(f"代理池 relay 对 {host} 不可用,回退直连: {reason}", step="check_proxy") + relay_fallback_warned = True + + def _update_relay_cookie_jar(resp: Any) -> None: + try: + for k, v in (resp.cookies or {}).items(): + key = str(k or "").strip() + if key: + relay_cookie_jar[key] = str(v or "") + except Exception: + pass + set_cookie_values: list[str] = [] + try: + values = resp.headers.get_list("set-cookie") # type: ignore[attr-defined] + if values: + set_cookie_values.extend(str(v or "") for v in values if str(v or "").strip()) + except Exception: + pass + if not set_cookie_values: + try: + set_cookie_raw = str(resp.headers.get("set-cookie") or "") + if set_cookie_raw.strip(): + set_cookie_values.append(set_cookie_raw) + except Exception: + pass + for set_cookie_raw in set_cookie_values: + try: + parsed_cookie = SimpleCookie() + parsed_cookie.load(set_cookie_raw) + for k, morsel in parsed_cookie.items(): + key = str(k or "").strip() + if key: + relay_cookie_jar[key] = str(morsel.value or "") + except Exception: + pass + try: + for k, v in relay_cookie_jar.items(): + s.cookies.set(k, v) + except Exception: + pass + + def _request_via_pool_relay(method: str, target_url: str, **kwargs: Any): + if not pool_relay_enabled: + raise RuntimeError("代理池 relay 未启用") + relay_retries_override = kwargs.pop("_relay_retries", None) + relay_params = { + "api_key": pool_relay_api_key, + "url": str(target_url), + "method": str(method or "GET").upper(), + "country": pool_relay_country, + } + target_params = kwargs.pop("params", None) + if target_params: + query_text = urlencode(target_params, doseq=True) + if query_text: + separator = "&" if "?" in relay_params["url"] else "?" + relay_params["url"] = f"{relay_params['url']}{separator}{query_text}" + + headers = dict(kwargs.pop("headers", {}) or {}) + if relay_cookie_jar and not any(str(k).lower() == "cookie" for k in headers.keys()): + headers["Cookie"] = "; ".join(f"{k}={v}" for k, v in relay_cookie_jar.items()) + kwargs.pop("proxies", None) + kwargs.setdefault("impersonate", "chrome") + kwargs.setdefault("http_version", DEFAULT_HTTP_VERSION) + kwargs.setdefault("timeout", 20) + + method_upper = relay_params["method"] + retry_count = max( + 1, + int( + relay_retries_override + if relay_retries_override is not None + else (pool_cfg.get("relay_request_retries") or POOL_RELAY_REQUEST_RETRIES) + ), + ) + last_error = "" + for i in range(retry_count): + try: + resp = _call_with_http_fallback( + lambda relay_endpoint, **call_kwargs: requests.request(method_upper, relay_endpoint, **call_kwargs), + pool_relay_url, + params=relay_params, + headers=headers or None, + **kwargs, + ) + _update_relay_cookie_jar(resp) + if resp.status_code >= 500 or resp.status_code == 429: + last_error = f"HTTP {resp.status_code}" + if i < retry_count - 1: + time.sleep(min(0.4 * (i + 1), 1.2)) + continue + return resp + except Exception as exc: + last_error = str(exc) + if i < retry_count - 1: + time.sleep(min(0.4 * (i + 1), 1.2)) + raise RuntimeError(f"代理池 relay 请求失败: {last_error or 'unknown error'}") + + def _ensure_openai_relay_ready() -> None: + nonlocal openai_relay_probe_done + if not pool_relay_enabled or relay_bypass_openai_hosts or openai_relay_probe_done: + return + openai_relay_probe_done = True + probe_url = "https://auth.openai.com/" + try: + probe_resp = _request_via_pool_relay( + "GET", + probe_url, + timeout=5, + allow_redirects=False, + _relay_retries=1, + ) + status = int(probe_resp.status_code or 0) + if status < 200 or status >= 400: + raise RuntimeError(f"HTTP {status}") + emitter.info("代理池 relay OpenAI 预检通过", step="check_proxy") + except Exception as exc: + _warn_relay_fallback(f"{exc} (OpenAI 预检)", probe_url) + + def _session_get(url: str, **kwargs: Any): + if pool_relay_enabled and not _should_bypass_relay_for_target(url): + try: + relay_resp = _request_via_pool_relay("GET", url, **kwargs) + if relay_resp.status_code < 500 and relay_resp.status_code != 429: + return relay_resp + raise RuntimeError(f"HTTP {relay_resp.status_code}") + except Exception as exc: + _warn_relay_fallback(str(exc), url) + kwargs["proxies"] = _fallback_proxies_for_relay_failure() + kwargs.setdefault("http_version", DEFAULT_HTTP_VERSION) + kwargs.setdefault("timeout", 20) + return _call_with_http_fallback(s.get, url, **kwargs) + if pool_relay_enabled and _should_bypass_relay_for_target(url): + kwargs["proxies"] = _fallback_proxies_for_relay_failure() + kwargs.setdefault("http_version", DEFAULT_HTTP_VERSION) + kwargs.setdefault("timeout", 20) + return _call_with_http_fallback(s.get, url, **kwargs) + kwargs["proxies"] = _next_proxies() + kwargs.setdefault("http_version", DEFAULT_HTTP_VERSION) + kwargs.setdefault("timeout", 15) + return _call_with_http_fallback(s.get, url, **kwargs) + + def _session_post(url: str, **kwargs: Any): + if pool_relay_enabled and not _should_bypass_relay_for_target(url): + try: + relay_resp = _request_via_pool_relay("POST", url, **kwargs) + if relay_resp.status_code < 500 and relay_resp.status_code != 429: + return relay_resp + raise RuntimeError(f"HTTP {relay_resp.status_code}") + except Exception as exc: + _warn_relay_fallback(str(exc), url) + kwargs["proxies"] = _fallback_proxies_for_relay_failure() + kwargs.setdefault("http_version", DEFAULT_HTTP_VERSION) + kwargs.setdefault("timeout", 20) + return _call_with_http_fallback(s.post, url, **kwargs) + if pool_relay_enabled and _should_bypass_relay_for_target(url): + kwargs["proxies"] = _fallback_proxies_for_relay_failure() + kwargs.setdefault("http_version", DEFAULT_HTTP_VERSION) + kwargs.setdefault("timeout", 20) + return _call_with_http_fallback(s.post, url, **kwargs) + kwargs["proxies"] = _next_proxies() + kwargs.setdefault("http_version", DEFAULT_HTTP_VERSION) + kwargs.setdefault("timeout", 15) + return _call_with_http_fallback(s.post, url, **kwargs) + + def _raw_get(url: str, **kwargs: Any): + if pool_relay_enabled and not _should_bypass_relay_for_target(url): + try: + relay_resp = _request_via_pool_relay("GET", url, **kwargs) + if relay_resp.status_code < 500 and relay_resp.status_code != 429: + return relay_resp + raise RuntimeError(f"HTTP {relay_resp.status_code}") + except Exception as exc: + _warn_relay_fallback(str(exc), url) + kwargs["proxies"] = _fallback_proxies_for_relay_failure() + kwargs.setdefault("http_version", DEFAULT_HTTP_VERSION) + kwargs.setdefault("impersonate", "chrome") + kwargs.setdefault("timeout", 20) + return _call_with_http_fallback(requests.get, url, **kwargs) + if pool_relay_enabled and _should_bypass_relay_for_target(url): + kwargs["proxies"] = _fallback_proxies_for_relay_failure() + kwargs.setdefault("http_version", DEFAULT_HTTP_VERSION) + kwargs.setdefault("impersonate", "chrome") + kwargs.setdefault("timeout", 20) + return _call_with_http_fallback(requests.get, url, **kwargs) + kwargs["proxies"] = _next_proxies() + kwargs.setdefault("http_version", DEFAULT_HTTP_VERSION) + kwargs.setdefault("impersonate", "chrome") + kwargs.setdefault("timeout", 15) + return _call_with_http_fallback(requests.get, url, **kwargs) + + def _raw_post(url: str, **kwargs: Any): + if pool_relay_enabled and not _should_bypass_relay_for_target(url): + try: + relay_resp = _request_via_pool_relay("POST", url, **kwargs) + if relay_resp.status_code < 500 and relay_resp.status_code != 429: + return relay_resp + raise RuntimeError(f"HTTP {relay_resp.status_code}") + except Exception as exc: + _warn_relay_fallback(str(exc), url) + kwargs["proxies"] = _fallback_proxies_for_relay_failure() + kwargs.setdefault("http_version", DEFAULT_HTTP_VERSION) + kwargs.setdefault("impersonate", "chrome") + kwargs.setdefault("timeout", 20) + return _call_with_http_fallback(requests.post, url, **kwargs) + if pool_relay_enabled and _should_bypass_relay_for_target(url): + kwargs["proxies"] = _fallback_proxies_for_relay_failure() + kwargs.setdefault("http_version", DEFAULT_HTTP_VERSION) + kwargs.setdefault("impersonate", "chrome") + kwargs.setdefault("timeout", 20) + return _call_with_http_fallback(requests.post, url, **kwargs) + kwargs["proxies"] = _next_proxies() + kwargs.setdefault("http_version", DEFAULT_HTTP_VERSION) + kwargs.setdefault("impersonate", "chrome") + kwargs.setdefault("timeout", 15) + return _call_with_http_fallback(requests.post, url, **kwargs) + + def _submit_callback_url_via_pool_relay( + *, + callback_url: str, + expected_state: str, + code_verifier: str, + redirect_uri: str = DEFAULT_REDIRECT_URI, + ) -> str: + cb = _parse_callback_url(callback_url) + if cb["error"]: + desc = cb["error_description"] + raise RuntimeError(f"oauth error: {cb['error']}: {desc}".strip()) + if not cb["code"]: + raise ValueError("callback url missing ?code=") + if not cb["state"]: + raise ValueError("callback url missing ?state=") + if cb["state"] != expected_state: + raise ValueError("state mismatch") + + token_resp = _request_via_pool_relay( + "POST", + TOKEN_URL, + headers={ + "Content-Type": "application/x-www-form-urlencoded", + "Accept": "application/json", + }, + data=urllib.parse.urlencode( + { + "grant_type": "authorization_code", + "client_id": CLIENT_ID, + "code": cb["code"], + "redirect_uri": redirect_uri, + "code_verifier": code_verifier, + } + ), + timeout=30, + ) + if token_resp.status_code != 200: + raise RuntimeError( + f"token exchange failed: {token_resp.status_code}: {str(token_resp.text or '')[:240]}" + ) + try: + token_json = token_resp.json() + except Exception: + token_json = json.loads(str(token_resp.text or "{}")) + + return _build_token_result(token_json, account_password=account_password) + + def _stopped() -> bool: + return stop_event is not None and stop_event.is_set() + + try: + # ------- 步骤1:网络环境检查 ------- + emitter.info("正在检查网络环境...", step="check_proxy") + try: + trace_text = "" + relay_error = "" + relay_used = False + if pool_cfg["enabled"]: + try: + trace_text = _trace_via_pool_relay(pool_cfg) + relay_used = True + except Exception as e: + relay_error = str(e) + if static_proxy: + emitter.warn(f"代理池 relay 检查失败,回退固定代理: {relay_error}", step="check_proxy") + else: + emitter.warn(f"代理池 relay 检查失败,尝试直连代理: {relay_error}", step="check_proxy") + if not trace_text: + trace_resp = _session_get("https://cloudflare.com/cdn-cgi/trace", timeout=10) + trace_text = trace_resp.text + trace = trace_text + loc_re = re.search(r"^loc=(.+)$", trace, re.MULTILINE) + loc = loc_re.group(1) if loc_re else None + ip_re = re.search(r"^ip=(.+)$", trace, re.MULTILINE) + current_ip = ip_re.group(1).strip() if ip_re else "" + if relay_used: + emitter.info("代理池 relay 连通检查成功", step="check_proxy") + emitter.info(f"当前 IP 所在地: {loc}", step="check_proxy") + if current_ip: + emitter.info(f"当前出口 IP: {current_ip}", step="check_proxy") + if loc == "CN" or loc == "HK": + emitter.error("检查代理哦 — 所在地不支持 (CN/HK)", step="check_proxy") + return None + emitter.success("网络环境检查通过", step="check_proxy") + _ensure_openai_relay_ready() + except Exception as e: + emitter.error(f"网络连接检查失败: {e}", step="check_proxy") + return None + + if _stopped(): + return None + + # ------- 步骤2:创建临时邮箱 ------- + if mail_provider is not None: + emitter.info("正在创建临时邮箱...", step="create_email") + try: + email, dev_token = mail_provider.create_mailbox( + proxy=static_proxy, + proxy_selector=mail_proxy_selector, + ) + except TypeError: + email, dev_token = mail_provider.create_mailbox(proxy=static_proxy) + else: + emitter.info("正在创建 Mail.tm 临时邮箱...", step="create_email") + email, dev_token = get_email_and_token( + static_proxies, + emitter, + proxy_selector=mail_proxies_selector, + ) + if not email or not dev_token: + emitter.error("临时邮箱创建失败", step="create_email") + return None + emitter.success(f"临时邮箱创建成功: {email}", step="create_email") + + # 生成随机密码(密码注册流程需要) + _pw_chars = string.ascii_letters + string.digits + "!@#$%&*" + account_password = "".join(secrets.choice(_pw_chars) for _ in range(16)) + + if _stopped(): + return None + + # ------- 步骤3:通过 chatgpt.com 建立注册会话 ------- + emitter.info("正在访问 ChatGPT 首页...", step="oauth_init") + _chatgpt_base = "https://chatgpt.com" + + # 3a: 访问首页,获取 cookies + _session_get(f"{_chatgpt_base}/", timeout=20) + + # 3b: 获取 CSRF Token + csrf_resp = _session_get( + f"{_chatgpt_base}/api/auth/csrf", + headers={"Accept": "application/json", "Referer": f"{_chatgpt_base}/"}, + timeout=15, + ) + try: + csrf_token = csrf_resp.json().get("csrfToken", "") + except Exception: + csrf_token = "" + if not csrf_token: + emitter.error("获取 CSRF Token 失败", step="oauth_init") + return None + + # 3c: 生成 Device ID + did = s.cookies.get("oai-did") or relay_cookie_jar.get("oai-did") or "" + if not did: + did = str(uuid.uuid4()) + relay_cookie_jar["oai-did"] = did + try: + s.cookies.set("oai-did", did, domain="chatgpt.com") + except Exception: + try: + s.cookies.set("oai-did", did) + except Exception: + pass + + # 3d: Signin 请求,获取 authorize URL + auth_session_id = str(uuid.uuid4()) + signin_params = urllib.parse.urlencode({ + "prompt": "login", + "ext-oai-did": did, + "auth_session_logging_id": auth_session_id, + "screen_hint": "login_or_signup", + "login_hint": email, + }) + signin_resp = _session_post( + f"{_chatgpt_base}/api/auth/signin/openai?{signin_params}", + headers={ + "Content-Type": "application/x-www-form-urlencoded", + "Accept": "application/json", + "Referer": f"{_chatgpt_base}/", + "Origin": _chatgpt_base, + }, + data=urllib.parse.urlencode({ + "callbackUrl": f"{_chatgpt_base}/", + "csrfToken": csrf_token, + "json": "true", + }), + timeout=20, + ) + try: + authorize_url = signin_resp.json().get("url", "") + except Exception: + authorize_url = "" + if not authorize_url: + emitter.error( + f"Signin 获取授权链接失败({signin_resp.status_code}): {str(signin_resp.text or '')[:220]}", + step="oauth_init", + ) + return None + emitter.info(f"OAuth 初始化状态: {signin_resp.status_code}", step="oauth_init") + + # 3e: 跟随 authorize 重定向,建立 auth.openai.com 会话 + auth_resp = _session_get( + authorize_url, + headers={ + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", + "Referer": f"{_chatgpt_base}/", + "Upgrade-Insecure-Requests": "1", + }, + timeout=20, + ) + final_url = str(auth_resp.url) if hasattr(auth_resp, "url") else "" + emitter.info(f"Authorize 重定向完成: {final_url[:120]}", step="oauth_init") + emitter.info(f"Device ID: {did}", step="oauth_init") + + if _stopped(): + return None + + # ------- 步骤4+5:密码注册(合并旧步骤4 Sentinel + 旧步骤5 注册) ------- + time.sleep(random.uniform(0.5, 1.0)) + emitter.info("正在提交注册表单(密码模式)...", step="signup") + _reg_headers = { + "referer": "https://auth.openai.com/create-account/password", + "accept": "application/json", + "content-type": "application/json", + "origin": "https://auth.openai.com", + } + _reg_headers.update(_trace_headers()) + signup_resp = _session_post( + "https://auth.openai.com/api/accounts/user/register", + headers=_reg_headers, + json={"username": email, "password": account_password}, + ) + emitter.info(f"注册表单提交状态: {signup_resp.status_code}", step="signup") + if signup_resp.status_code != 200: + emitter.error( + f"注册表单提交失败(状态码 {signup_resp.status_code}): {str(signup_resp.text or '')[:220]}", + step="signup", + ) + return None + + # ------- 步骤6:发送 OTP 验证码 ------- + time.sleep(random.uniform(0.3, 0.8)) + emitter.info("正在发送邮箱验证码...", step="send_otp") + otp_resp = _session_get( + "https://auth.openai.com/api/accounts/email-otp/send", + headers={ + "referer": "https://auth.openai.com/create-account/password", + "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", + "upgrade-insecure-requests": "1", + }, + ) + emitter.info(f"验证码发送状态: {otp_resp.status_code}", step="send_otp") + if otp_resp.status_code == 409: + emitter.warn(f"send_otp 409 响应: {str(otp_resp.text or '')[:220]}", step="send_otp") + + if otp_resp.status_code != 200: + emitter.error(f"验证码发送失败(状态码 {otp_resp.status_code}): {str(otp_resp.text or '')[:220]}", step="send_otp") + return None + + if _stopped(): + return None + + # ------- 步骤7:轮询邮箱拿验证码 ------- + if mail_provider is not None: + try: + code = mail_provider.wait_for_otp( + dev_token, + email, + proxy=static_proxy, + proxy_selector=mail_proxy_selector, + stop_event=stop_event, + ) + except TypeError: + code = mail_provider.wait_for_otp( + dev_token, + email, + proxy=static_proxy, + stop_event=stop_event, + ) + else: + code = get_oai_code( + dev_token, + email, + static_proxies, + emitter, + stop_event, + proxy_selector=mail_proxies_selector, + ) + if not code: + return None + + if _stopped(): + return None + + # ------- 步骤8:提交验证码 ------- + time.sleep(random.uniform(0.3, 0.8)) + emitter.info("正在验证 OTP...", step="verify_otp") + _otp_headers = { + "referer": "https://auth.openai.com/email-verification", + "accept": "application/json", + "content-type": "application/json", + "origin": "https://auth.openai.com", + } + _otp_headers.update(_trace_headers()) + code_resp = _session_post( + "https://auth.openai.com/api/accounts/email-otp/validate", + headers=_otp_headers, + json={"code": code}, + ) + emitter.info(f"验证码校验状态: {code_resp.status_code}", step="verify_otp") + if code_resp.status_code != 200: + emitter.error( + f"验证码校验失败(状态码 {code_resp.status_code}): {str(code_resp.text or '')[:220]}", + step="verify_otp", + ) + return None + + if _stopped(): + return None + + # ------- 步骤9:创建账户 ------- + time.sleep(random.uniform(0.5, 1.5)) + emitter.info("正在创建账户信息...", step="create_account") + _rand_first = random.choice([ + "James", "Emma", "Liam", "Olivia", "Noah", "Ava", "Ethan", "Sophia", + "Lucas", "Mia", "Mason", "Isabella", "Logan", "Charlotte", "Alexander", + "Amelia", "Benjamin", "Harper", "William", "Evelyn", "Henry", "Abigail", + ]) + _rand_last = random.choice([ + "Smith", "Johnson", "Brown", "Davis", "Wilson", "Moore", "Taylor", + "Clark", "Hall", "Young", "Anderson", "Thomas", "Jackson", "White", + ]) + _rand_name = f"{_rand_first} {_rand_last}" + _rand_bday = f"{random.randint(1985, 2002)}-{random.randint(1, 12):02d}-{random.randint(1, 28):02d}" + _ca_headers = { + "referer": "https://auth.openai.com/about-you", + "accept": "application/json", + "content-type": "application/json", + "origin": "https://auth.openai.com", + } + _ca_headers.update(_trace_headers()) + create_account_resp = _session_post( + "https://auth.openai.com/api/accounts/create_account", + headers=_ca_headers, + json={"name": _rand_name, "birthdate": _rand_bday}, + ) + create_account_status = create_account_resp.status_code + emitter.info(f"账户创建状态: {create_account_status}", step="create_account") + + if create_account_status != 200: + emitter.error(create_account_resp.text, step="create_account") + return None + + emitter.success("账户创建成功!", step="create_account") + + # 跟随 callback URL 完成注册流程 + try: + _ca_data = create_account_resp.json() if create_account_resp.text else {} + except Exception: + _ca_data = {} + _callback_url = ( + _ca_data.get("continue_url") + or _ca_data.get("url") + or _ca_data.get("redirect_url") + or "" + ) + if _callback_url: + emitter.info("正在完成注册回调...", step="create_account") + _session_get( + _callback_url, + headers={ + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", + "Upgrade-Insecure-Requests": "1", + }, + timeout=20, + ) + + if _stopped(): + return None + + # ------- 步骤10+11:完整 OAuth 登录流程获取 Token ------- + emitter.info("正在通过 OAuth 登录获取 Token...", step="get_token") + + # 确保 auth 域也有 oai-did cookie + try: + s.cookies.set("oai-did", did, domain=".auth.openai.com") + s.cookies.set("oai-did", did, domain="auth.openai.com") + except Exception: + pass + + # 10a: 生成 PKCE 参数和 authorize URL + oauth = generate_oauth_url() + + # 10b: Sentinel PoW token 生成器(纯 Python) + class _SentinelGen: + MAX_ATTEMPTS = 500000 + ERROR_PREFIX = "wQ8Lk5FbGpA2NcR9dShT6gYjU7VxZ4D" + def __init__(self, dev_id, ua): + self.dev_id = dev_id + self.ua = ua + self.req_seed = str(random.random()) + self.sid = str(uuid.uuid4()) + @staticmethod + def _fnv1a(text): + h = 2166136261 + for ch in text: + h ^= ord(ch); h = (h * 16777619) & 0xFFFFFFFF + h ^= (h >> 16); h = (h * 2246822507) & 0xFFFFFFFF + h ^= (h >> 13); h = (h * 3266489909) & 0xFFFFFFFF + h ^= (h >> 16); return format(h & 0xFFFFFFFF, "08x") + def _cfg(self): + now_s = time.strftime("%a %b %d %Y %H:%M:%S GMT+0000 (Coordinated Universal Time)", time.gmtime()) + perf = random.uniform(1000, 50000) + return ["1920x1080", now_s, 4294705152, random.random(), self.ua, + "https://sentinel.openai.com/sentinel/20260124ceb8/sdk.js", + None, None, "en-US", "en-US,en", random.random(), + random.choice(["vendorSub","productSub","hardwareConcurrency","cookieEnabled"]) + "-undefined", + random.choice(["location","URL","compatMode"]), + random.choice(["Object","Function","Array","Number"]), + perf, self.sid, "", random.choice([4,8,12,16]), time.time()*1000 - perf] + @staticmethod + def _b64(data): + return base64.b64encode(json.dumps(data, separators=(",",":")).encode()).decode() + def _solve(self, seed, diff, cfg, nonce): + cfg[3] = nonce; cfg[9] = round((time.time() - self._t0) * 1000) + d = self._b64(cfg); h = self._fnv1a(seed + d) + return (d + "~S") if h[:len(diff)] <= diff else None + def gen_token(self, seed=None, diff="0"): + seed = seed or self.req_seed; self._t0 = time.time(); cfg = self._cfg() + for i in range(self.MAX_ATTEMPTS): + r = self._solve(seed, str(diff), cfg, i) + if r: return "gAAAAAB" + r + return "gAAAAAB" + self.ERROR_PREFIX + self._b64(str(None)) + def gen_req_token(self): + cfg = self._cfg(); cfg[3] = 1; cfg[9] = round(random.uniform(5, 50)) + return "gAAAAAC" + self._b64(cfg) + + _sentinel = _SentinelGen(did, _chrome_ua) + + def _build_sentinel(flow): + req_body = json.dumps({"p": _sentinel.gen_req_token(), "id": did, "flow": flow}) + sen_resp = _session_post( + "https://sentinel.openai.com/backend-api/sentinel/req", + headers={ + "Content-Type": "text/plain;charset=UTF-8", + "Origin": "https://sentinel.openai.com", + "Referer": "https://sentinel.openai.com/backend-api/sentinel/frame.html", + }, + data=req_body, + ) + if sen_resp.status_code != 200: + return None + try: + ch = sen_resp.json() + except Exception: + return None + c_val = ch.get("token", "") + if not c_val: + return None + pow_d = ch.get("proofofwork") or {} + if pow_d.get("required") and pow_d.get("seed"): + p_val = _sentinel.gen_token(seed=pow_d["seed"], diff=pow_d.get("difficulty", "0")) + else: + p_val = _sentinel.gen_req_token() + return json.dumps({"p": p_val, "t": "", "c": c_val, "id": did, "flow": flow}, separators=(",",":")) + + def _oauth_headers(referer): + h = {"Accept": "application/json", "Content-Type": "application/json", + "Origin": "https://auth.openai.com", "Referer": referer, "oai-device-id": did} + h.update(_trace_headers()) + return h + + # 10c: GET /oauth/authorize — 建立 OAuth 会话 + emitter.info("OAuth 1/5: 初始化授权...", step="get_token") + _session_get(oauth.auth_url, timeout=30) + + if _stopped(): + return None + + # 10d: POST authorize/continue — 提交邮箱 + emitter.info("OAuth 2/5: 提交邮箱...", step="get_token") + _sen_ac = _build_sentinel("authorize_continue") + if not _sen_ac: + emitter.error("Sentinel token (authorize_continue) 获取失败", step="get_token") + return None + _ac_headers = _oauth_headers("https://auth.openai.com/log-in") + _ac_headers["openai-sentinel-token"] = _sen_ac + _ac_resp = _session_post( + "https://auth.openai.com/api/accounts/authorize/continue", + headers=_ac_headers, + json={"username": {"kind": "email", "value": email}}, + ) + emitter.info(f"authorize/continue -> {_ac_resp.status_code}", step="get_token") + if _ac_resp.status_code != 200: + emitter.error(f"authorize/continue 失败: {str(_ac_resp.text or '')[:200]}", step="get_token") + return None + + if _stopped(): + return None + + # 10e: POST password/verify — 提交密码 + emitter.info("OAuth 3/5: 验证密码...", step="get_token") + _sen_pw = _build_sentinel("password_verify") + if not _sen_pw: + emitter.error("Sentinel token (password_verify) 获取失败", step="get_token") + return None + _pw_headers = _oauth_headers("https://auth.openai.com/log-in/password") + _pw_headers["openai-sentinel-token"] = _sen_pw + _pw_resp = _session_post( + "https://auth.openai.com/api/accounts/password/verify", + headers=_pw_headers, + json={"password": account_password}, + ) + emitter.info(f"password/verify -> {_pw_resp.status_code}", step="get_token") + if _pw_resp.status_code != 200: + emitter.error(f"password/verify 失败: {str(_pw_resp.text or '')[:200]}", step="get_token") + return None + + try: + _pw_data = _pw_resp.json() + except Exception: + _pw_data = {} + _consent_url = str(_pw_data.get("continue_url") or "").strip() + _page_type = str((_pw_data.get("page") or {}).get("type", "")).strip() + emitter.info(f"password/verify page={_page_type or '-'} next={(_consent_url or '-')[:140]}", step="get_token") + + # OAuth 阶段可能需要第二次邮箱 OTP 验证 + _need_oauth_otp = ( + _page_type == "email_otp_verification" + or "email-verification" in (_consent_url or "") + or "email-otp" in (_consent_url or "") + ) + if _need_oauth_otp: + emitter.info("OAuth 需要邮箱 OTP 验证...", step="get_token") + if not dev_token or mail_provider is None: + emitter.error("OAuth OTP 验证需要邮箱 token,但不可用", step="get_token") + return None + + _otp_ok = False + _otp_deadline = time.time() + 120 + _tried_codes: set = set() + while time.time() < _otp_deadline and not _otp_ok: + if _stopped(): + return None + try: + _otp_code2 = mail_provider.wait_for_otp( + dev_token, email, proxy=static_proxy, + proxy_selector=mail_proxy_selector, stop_event=stop_event, + ) + except TypeError: + _otp_code2 = mail_provider.wait_for_otp( + dev_token, email, proxy=static_proxy, stop_event=stop_event, + ) + if not _otp_code2 or _otp_code2 in _tried_codes: + time.sleep(2) + continue + _tried_codes.add(_otp_code2) + emitter.info(f"OAuth OTP 尝试: {_otp_code2}", step="get_token") + _otp2_h = _oauth_headers("https://auth.openai.com/email-verification") + _otp2_resp = _session_post( + "https://auth.openai.com/api/accounts/email-otp/validate", + headers=_otp2_h, + json={"code": _otp_code2}, + ) + emitter.info(f"OAuth OTP validate -> {_otp2_resp.status_code}", step="get_token") + if _otp2_resp.status_code == 200: + try: + _otp2_data = _otp2_resp.json() + except Exception: + _otp2_data = {} + _consent_url = str(_otp2_data.get("continue_url") or "").strip() or _consent_url + _page_type = str((_otp2_data.get("page") or {}).get("type", "")).strip() or _page_type + emitter.info(f"OAuth OTP 验证通过 page={_page_type or '-'} next={(_consent_url or '-')[:140]}", step="get_token") + _otp_ok = True + break + time.sleep(2) + + if not _otp_ok: + emitter.error(f"OAuth OTP 验证失败,已尝试 {len(_tried_codes)} 个验证码", step="get_token") + return None + + if _stopped(): + return None + + # 10f: Workspace/consent/org 处理 + 提取 code(对标参考代码) + _AUTH = "https://auth.openai.com" + + def _extract_code(url): + if not url or "code=" not in url: + return None + try: + return urllib.parse.parse_qs(urllib.parse.urlparse(url).query).get("code", [None])[0] + except Exception: + return None + + def _follow_for_code(start_url): + """手动跟随重定向链,逐步检查 Location 中是否含 code""" + url = start_url + for _ in range(12): + try: + r = _session_get(url, allow_redirects=False, timeout=15) + except Exception as e: + # curl_cffi 连接 localhost 会抛异常,URL 可能在异常信息中 + m = re.search(r'(https?://localhost[^\s\'"]+)', str(e)) + if m: + return _extract_code(m.group(1)) + return None + if r.status_code in (301, 302, 303, 307, 308): + loc = r.headers.get("Location", "") + if not loc: + break + next_url = urllib.parse.urljoin(url, loc) + c = _extract_code(next_url) + if c: + return c + url = next_url + continue + break + return None + + def _ws_org_select(consent_ref): + """完整的 workspace + organization 选择流程""" + # 解析 session cookie 获取 workspace + _ck_data = None + _auth_ck = s.cookies.get("oai-client-auth-session") or relay_cookie_jar.get("oai-client-auth-session") or "" + if _auth_ck: + try: + _ck_data = _decode_jwt_segment(_auth_ck.split(".")[0]) + except Exception: + pass + if not _ck_data: + emitter.info("无法解码 auth session cookie", step="workspace") + return None + + _ws_list = _ck_data.get("workspaces") or [] + _ws_id = str((_ws_list[0] or {}).get("id") or "").strip() if _ws_list else "" + if not _ws_id: + emitter.info("session 中没有 workspace", step="workspace") + return None + + emitter.info(f"选择 workspace: {_ws_id}", step="workspace") + _ws_h = _oauth_headers(consent_ref) + _ws_resp = _session_post( + f"{_AUTH}/api/accounts/workspace/select", + headers=_ws_h, + json={"workspace_id": _ws_id}, + allow_redirects=False, + ) + emitter.info(f"workspace/select -> {_ws_resp.status_code}", step="workspace") + + # 如果是重定向,直接提取 code + if _ws_resp.status_code in (301, 302, 303, 307, 308): + loc = _ws_resp.headers.get("Location", "") + if loc.startswith("/"): + loc = f"{_AUTH}{loc}" + c = _extract_code(loc) + if c: + return c + return _follow_for_code(loc) + + if _ws_resp.status_code != 200: + return None + + try: + _ws_data = _ws_resp.json() + except Exception: + return None + + _ws_next = str(_ws_data.get("continue_url") or "").strip() + _ws_page = str((_ws_data.get("page") or {}).get("type", "")) + _orgs = (_ws_data.get("data") or {}).get("orgs") or [] + emitter.info(f"workspace/select page={_ws_page or '-'} orgs={len(_orgs)} next={(_ws_next or '-')[:140]}", step="workspace") + + # Organization 选择 + if _orgs: + _org_id = (_orgs[0] or {}).get("id") + _projects = (_orgs[0] or {}).get("projects") or [] + _proj_id = (_projects[0] or {}).get("id") if _projects else None + if _org_id: + _org_body = {"org_id": _org_id} + if _proj_id: + _org_body["project_id"] = _proj_id + _org_ref = _ws_next if _ws_next and _ws_next.startswith("http") else f"{_AUTH}{_ws_next}" if _ws_next else consent_ref + _org_h = _oauth_headers(_org_ref) + emitter.info(f"选择 organization: {_org_id}", step="workspace") + _org_resp = _session_post( + f"{_AUTH}/api/accounts/organization/select", + headers=_org_h, + json=_org_body, + allow_redirects=False, + ) + emitter.info(f"organization/select -> {_org_resp.status_code}", step="workspace") + + if _org_resp.status_code in (301, 302, 303, 307, 308): + loc = _org_resp.headers.get("Location", "") + if loc.startswith("/"): + loc = f"{_AUTH}{loc}" + c = _extract_code(loc) + if c: + return c + return _follow_for_code(loc) + + if _org_resp.status_code == 200: + try: + _org_data = _org_resp.json() + except Exception: + _org_data = {} + _org_next = str(_org_data.get("continue_url") or "").strip() + if _org_next: + if _org_next.startswith("/"): + _org_next = f"{_AUTH}{_org_next}" + c = _extract_code(_org_next) + if c: + return c + return _follow_for_code(_org_next) + + # 无 org 或 org 选择后仍无 code,跟随 ws_next + if _ws_next: + if _ws_next.startswith("/"): + _ws_next = f"{_AUTH}{_ws_next}" + c = _extract_code(_ws_next) + if c: + return c + return _follow_for_code(_ws_next) + + return None + + _code = None + + # 规范化 consent_url + if _consent_url and _consent_url.startswith("/"): + _consent_url = f"{_AUTH}{_consent_url}" + if not _consent_url and "consent" in _page_type: + _consent_url = f"{_AUTH}/sign-in-with-chatgpt/codex/consent" + + # 先从 URL 直接提取 + if _consent_url: + _code = _extract_code(_consent_url) + + # 跟随 consent_url 重定向 + if not _code and _consent_url: + emitter.info("OAuth 4/5: 跟随 consent URL...", step="get_token") + _code = _follow_for_code(_consent_url) + + # workspace + organization 选择 + _consent_hint = any(kw in (_consent_url or "") for kw in ["consent", "workspace", "organization", "sign-in-with"]) + _consent_hint = _consent_hint or any(kw in _page_type for kw in ["consent", "organization"]) + if not _code and (_consent_hint or not _consent_url): + emitter.info("OAuth 4/5: 处理 workspace/org...", step="workspace") + _ws_ref = _consent_url or f"{_AUTH}/sign-in-with-chatgpt/codex/consent" + _code = _ws_org_select(_ws_ref) + + # 回退 + if not _code: + emitter.info("OAuth 4/5: 回退 consent 路径...", step="get_token") + _code = _ws_org_select(f"{_AUTH}/sign-in-with-chatgpt/codex/consent") + if not _code: + _code = _follow_for_code(f"{_AUTH}/sign-in-with-chatgpt/codex/consent") + + if not _code: + emitter.error("未能获取 OAuth authorization code", step="get_token") + try: s.close() + except: pass + return None + + # 10g: POST /oauth/token — 用 code 换取 Token + emitter.info("OAuth 5/5: 交换 Token...", step="get_token") + _token_resp = _session_post( + TOKEN_URL, + headers={"Content-Type": "application/x-www-form-urlencoded", "Accept": "application/json"}, + data=urllib.parse.urlencode({ + "grant_type": "authorization_code", + "code": _code, + "redirect_uri": oauth.redirect_uri, + "client_id": CLIENT_ID, + "code_verifier": oauth.code_verifier, + }), + timeout=30, + ) + if _token_resp.status_code != 200: + emitter.error(f"Token 交换失败({_token_resp.status_code}): {str(_token_resp.text or '')[:200]}", step="get_token") + try: s.close() + except: pass + return None + + try: + _token_json = _token_resp.json() + except Exception: + _token_json = json.loads(str(_token_resp.text or "{}")) + + emitter.success("Token 获取成功!", step="get_token") + try: s.close() + except: pass + return _build_token_result(_token_json, account_password=account_password) + + except Exception as e: + emitter.error(f"运行时发生错误: {e}", step="runtime") + try: s.close() + except: pass + return None + +# ========================================== +# CLI 入口(兼容直接运行) +# ========================================== + + +def main() -> None: + parser = argparse.ArgumentParser(description="OpenAI 账号池编排器脚本") + parser.add_argument( + "--proxy", default=None, help="代理地址,如 http://127.0.0.1:7897" + ) + parser.add_argument("--once", action="store_true", help="只运行一次") + parser.add_argument("--sleep-min", type=int, default=5, help="循环模式最短等待秒数") + parser.add_argument( + "--sleep-max", type=int, default=30, help="循环模式最长等待秒数" + ) + args = parser.parse_args() + + sleep_min = max(1, args.sleep_min) + sleep_max = max(sleep_min, args.sleep_max) + + os.makedirs(TOKENS_DIR, exist_ok=True) + + try: + config_path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "config", "sync_config.json") + with open(config_path, "r", encoding="utf-8") as f: + sync_cfg = json.load(f) + except Exception: + sync_cfg = {} + + cpa_base_url = str(sync_cfg.get("cpa_base_url") or "").strip() + cpa_token = str(sync_cfg.get("cpa_token") or "").strip() + + pool_maintainer = None + if cpa_base_url and cpa_token: + try: + from .pool_maintainer import PoolMaintainer + pool_maintainer = PoolMaintainer( + cpa_base_url=cpa_base_url, + cpa_token=cpa_token, + ) + except Exception as e: + print(f"[-] 初始化 PoolMaintainer 失败: {e}") + + count = 0 + print("[Info] OpenAI 账号池编排器 - CLI 模式") + + while True: + count += 1 + print( + f"\n[{datetime.now().strftime('%H:%M:%S')}] >>> 开始第 {count} 次注册流程 <<<" + ) + + try: + token_json = run(args.proxy) + + if token_json: + try: + t_data = json.loads(token_json) + fname_email = t_data.get("email", "unknown").replace("@", "_") + except Exception: + fname_email = "unknown" + t_data = {} + + file_name = f"token_{fname_email}_{time.time_ns()}.json" + file_path = os.path.join(TOKENS_DIR, file_name) + + _write_text_atomic(file_path, token_json) + + print(f"[*] 成功! Token 已保存至: {file_path}") + + if pool_maintainer and t_data: + print(f"[*] 正在尝试上传到 CPA...") + try: + cpa_ok = pool_maintainer.upload_token(file_name, t_data, proxy=args.proxy or "") + upload_email = t_data.get('email', fname_email) + if cpa_ok: + print(f"[+] CPA 上传成功: {upload_email}") + else: + print(f"[-] CPA 上传失败: {upload_email}") + except Exception as e: + print(f"[-] CPA 上传抛出异常: {e}") + else: + print("[-] 本次注册失败。") + + except Exception as e: + print(f"[Error] 发生未捕获异常: {e}") + + if args.once: + break + + wait_time = random.randint(sleep_min, sleep_max) + print(f"[*] 休息 {wait_time} 秒...") + time.sleep(wait_time) + + +if __name__ == "__main__": + main() + diff --git a/openai_pool_orchestrator/server.py b/openai_pool_orchestrator/server.py new file mode 100755 index 0000000..c2ea221 --- /dev/null +++ b/openai_pool_orchestrator/server.py @@ -0,0 +1,3556 @@ +""" +FastAPI 后端服务 +提供 REST API + SSE 实时日志推送 +""" + +import asyncio +import copy +import json +import re +import os +import queue +import random +import threading +import tempfile +import time +import urllib.request +import urllib.error +import uuid +from datetime import datetime +from pathlib import Path +from typing import Any, AsyncGenerator, Dict, List, Optional + +import uvicorn +from fastapi import FastAPI, HTTPException, Request +from fastapi.concurrency import run_in_threadpool +from fastapi.responses import HTMLResponse, StreamingResponse +from fastapi.staticfiles import StaticFiles +from pydantic import BaseModel, Field + +from . import __version__, TOKENS_DIR, CONFIG_FILE, STATE_FILE, STATIC_DIR, DATA_DIR +from .register import EventEmitter, run, _fetch_proxy_from_pool +from .mail_providers import create_provider, MultiMailRouter +from .pool_maintainer import PoolMaintainer, Sub2ApiMaintainer + +# ========================================== +# 同步配置(内存持久化到 data/sync_config.json) +# ========================================== + +# CONFIG_FILE 和 TOKENS_DIR 已从包 __init__.py 导入 + + +_config_lock = threading.RLock() +_service_shutdown_event = threading.Event() +_sub2api_accounts_cache_lock = threading.Lock() +_sub2api_accounts_cache: Dict[str, Any] = { + "signature": "", + "ts": 0.0, + "inventory": None, +} + +SUB2API_MAINTAIN_ACTION_DEFAULTS: Dict[str, bool] = { + "refresh_abnormal_accounts": True, + "delete_abnormal_accounts": True, + "dedupe_duplicate_accounts": True, +} + + +def _as_bool(value: Any, default: bool = False) -> bool: + if isinstance(value, bool): + return value + if value is None: + return default + if isinstance(value, (int, float)): + return bool(value) + text = str(value).strip().lower() + if text in ("1", "true", "yes", "on"): + return True + if text in ("0", "false", "no", "off", ""): + return False + return default + + +def _normalize_sub2api_maintain_actions(raw: Any) -> Dict[str, bool]: + source = raw if isinstance(raw, dict) else {} + return { + key: _as_bool(source.get(key, default), default=default) + for key, default in SUB2API_MAINTAIN_ACTION_DEFAULTS.items() + } + + +def _get_sub2api_maintain_actions(cfg: Optional[Dict[str, Any]] = None) -> Dict[str, bool]: + config = cfg if cfg is not None else _get_sync_config() + return _normalize_sub2api_maintain_actions(config.get("sub2api_maintain_actions")) + + +def _describe_sub2api_maintain_actions(actions: Optional[Dict[str, bool]] = None) -> str: + normalized = _normalize_sub2api_maintain_actions(actions) + labels: List[str] = [] + if normalized["refresh_abnormal_accounts"]: + labels.append("异常测活") + if normalized["delete_abnormal_accounts"]: + labels.append("异常清理") + if normalized["dedupe_duplicate_accounts"]: + labels.append("重复清理") + return "、".join(labels) if labels else "无动作" + + +def _format_sub2api_maintain_result_message(result: Dict[str, Any], *, auto: bool = False) -> str: + prefix = "自动维护" if auto else "维护完成" + actions_text = _describe_sub2api_maintain_actions(result.get("actions")) + return ( + f"[Sub2Api] {prefix}({actions_text}): 异常 {result.get('error_count', 0)}, " + f"刷新恢复 {result.get('refreshed', 0)}, " + f"重复组 {result.get('duplicate_groups', 0)}, " + f"删除 {result.get('deleted_ok', 0)}(失败 {result.get('deleted_fail', 0)}), " + f"耗时 {round((result.get('duration_ms', 0) or 0) / 1000, 2)}s" + ) + + +def _clear_sub2api_accounts_cache() -> None: + with _sub2api_accounts_cache_lock: + _sub2api_accounts_cache["signature"] = "" + _sub2api_accounts_cache["ts"] = 0.0 + _sub2api_accounts_cache["inventory"] = None + + +def _build_sub2api_accounts_cache_signature(cfg: Optional[Dict[str, Any]] = None) -> str: + config = cfg or _get_sync_config() + signature_payload = { + "base_url": str(config.get("base_url", "") or "").strip(), + "email": str(config.get("email", "") or "").strip().lower(), + "sub2api_min_candidates": int(config.get("sub2api_min_candidates", 200) or 200), + } + return json.dumps(signature_payload, ensure_ascii=False, sort_keys=True) + + +def _get_sub2api_accounts_inventory_snapshot( + sm: Sub2ApiMaintainer, + cfg: Optional[Dict[str, Any]] = None, + *, + timeout: int = 15, + ttl_seconds: int = 12, +) -> Dict[str, Any]: + signature = _build_sub2api_accounts_cache_signature(cfg) + now = time.time() + with _sub2api_accounts_cache_lock: + cached_signature = str(_sub2api_accounts_cache.get("signature") or "") + cached_ts = float(_sub2api_accounts_cache.get("ts") or 0.0) + cached_inventory = _sub2api_accounts_cache.get("inventory") + if ( + cached_signature == signature + and isinstance(cached_inventory, dict) + and (now - cached_ts) <= ttl_seconds + ): + return copy.deepcopy(cached_inventory) + + inventory = sm.list_account_inventory(timeout=timeout) + with _sub2api_accounts_cache_lock: + _sub2api_accounts_cache["signature"] = signature + _sub2api_accounts_cache["ts"] = now + _sub2api_accounts_cache["inventory"] = copy.deepcopy(inventory) + return inventory + + +def _filter_sub2api_account_items(items: List[Dict[str, Any]], status: str = "all", keyword: str = "") -> List[Dict[str, Any]]: + normalized_status = str(status or "all").strip().lower() or "all" + keyword_norm = str(keyword or "").strip().lower() + abnormal_statuses = {"error", "disabled"} + filtered: List[Dict[str, Any]] = [] + + for item in items: + item_status = str(item.get("status") or "").strip().lower() + is_abnormal = item_status in abnormal_statuses + is_duplicate = bool(item.get("is_duplicate")) + + if normalized_status == "normal" and is_abnormal: + continue + if normalized_status == "abnormal" and not is_abnormal: + continue + if normalized_status == "error" and item_status != "error": + continue + if normalized_status == "disabled" and item_status != "disabled": + continue + if normalized_status == "duplicate" and not is_duplicate: + continue + + if keyword_norm: + email = str(item.get("email") or "").lower() + name = str(item.get("name") or "").lower() + account_id = str(item.get("id") or "").lower() + if keyword_norm not in email and keyword_norm not in name and keyword_norm not in account_id: + continue + + filtered.append(item) + + return filtered + + +def _paginate_sub2api_account_items( + items: List[Dict[str, Any]], page: int = 1, page_size: int = 20, +) -> Dict[str, Any]: + safe_page_size = max(10, min(int(page_size or 20), 100)) + total = len(items) + total_pages = max(1, (total + safe_page_size - 1) // safe_page_size) + safe_page = max(1, min(int(page or 1), total_pages)) + start = (safe_page - 1) * safe_page_size + end = start + safe_page_size + return { + "items": items[start:end], + "page": safe_page, + "page_size": safe_page_size, + "filtered_total": total, + "total_pages": total_pages, + } + + +def _write_json_atomic(path: Path, payload: Dict[str, Any]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + fd, tmp_path = tempfile.mkstemp(prefix=f".{path.stem}_", suffix=path.suffix, dir=str(path.parent)) + try: + with os.fdopen(fd, "w", encoding="utf-8") as handle: + json.dump(payload, handle, ensure_ascii=False, indent=2) + handle.flush() + os.fsync(handle.fileno()) + os.replace(tmp_path, path) + finally: + try: + if os.path.exists(tmp_path): + os.remove(tmp_path) + except OSError: + pass + + +def _load_sync_config() -> Dict[str, Any]: + if CONFIG_FILE.exists(): + try: + return json.loads(CONFIG_FILE.read_text(encoding="utf-8")) + except Exception: + pass + return { + "base_url": "", "bearer_token": "", "account_name": "AutoReg", "auto_sync": False, + "cpa_base_url": "", "cpa_token": "", "min_candidates": 800, + "used_percent_threshold": 95, "auto_maintain": False, "maintain_interval_minutes": 30, + "upload_mode": "snapshot", + "mail_provider": "mailtm", + "mail_config": {"api_base": "https://api.mail.tm", "api_key": "", "bearer_token": ""}, + "sub2api_min_candidates": 200, + "sub2api_auto_maintain": False, + "sub2api_maintain_interval_minutes": 30, + "sub2api_maintain_actions": copy.deepcopy(SUB2API_MAINTAIN_ACTION_DEFAULTS), + "proxy": "", + "auto_register": False, + "proxy_pool_enabled": True, + "proxy_pool_api_url": "https://zenproxy.top/api/fetch", + "proxy_pool_auth_mode": "query", + "proxy_pool_api_key": "19c0ec43-8f76-4c97-81bc-bcda059eeba4", + "proxy_pool_count": 1, + "proxy_pool_country": "US", + } + + +def _normalize_config(cfg: Dict[str, Any]) -> Dict[str, Any]: + """将旧的单邮箱提供商配置迁移到多提供商格式,含类型校验""" + cfg = copy.deepcopy(cfg or {}) + legacy = str(cfg.get("mail_provider", "mailtm") or "mailtm").strip().lower() + legacy_cfg = cfg.get("mail_config") or {} + if not isinstance(legacy_cfg, dict): + legacy_cfg = {} + + raw_providers = cfg.get("mail_providers") + providers = raw_providers if isinstance(raw_providers, list) else [] + providers = [str(n).strip().lower() for n in providers if str(n).strip()] + if not providers: + providers = [legacy] + + raw_cfgs = cfg.get("mail_provider_configs") + provider_cfgs = raw_cfgs if isinstance(raw_cfgs, dict) else {} + for name in providers: + if name not in provider_cfgs or not isinstance(provider_cfgs.get(name), dict): + provider_cfgs[name] = {} + if legacy in provider_cfgs: + for k, v in legacy_cfg.items(): + provider_cfgs[legacy].setdefault(k, v) + + strategy = str(cfg.get("mail_strategy", "round_robin") or "round_robin").strip().lower() + if strategy not in ("round_robin", "random", "failover"): + strategy = "round_robin" + + cfg["mail_providers"] = providers + cfg["mail_provider_configs"] = provider_cfgs + cfg["mail_strategy"] = strategy + cfg["mail_provider"] = providers[0] + upload_mode = str(cfg.get("upload_mode", "snapshot") or "snapshot").strip().lower() + if upload_mode not in ("snapshot", "decoupled"): + upload_mode = "snapshot" + cfg["upload_mode"] = upload_mode + cfg["auto_sync"] = _as_bool(cfg.get("auto_sync", False), default=False) + cfg["auto_maintain"] = _as_bool(cfg.get("auto_maintain", False), default=False) + cfg["sub2api_auto_maintain"] = _as_bool(cfg.get("sub2api_auto_maintain", False), default=False) + cfg["sub2api_maintain_actions"] = _normalize_sub2api_maintain_actions(cfg.get("sub2api_maintain_actions")) + cfg["multithread"] = _as_bool(cfg.get("multithread", False), default=False) + cfg["auto_register"] = _as_bool(cfg.get("auto_register", False), default=False) + try: + cfg["thread_count"] = max(1, min(int(cfg.get("thread_count", 3)), 10)) + except (ValueError, TypeError): + cfg["thread_count"] = 3 + cfg["proxy_pool_enabled"] = _as_bool(cfg.get("proxy_pool_enabled", True), default=True) + proxy_pool_api_url = str(cfg.get("proxy_pool_api_url", "https://zenproxy.top/api/fetch") or "").strip() + cfg["proxy_pool_api_url"] = proxy_pool_api_url or "https://zenproxy.top/api/fetch" + proxy_pool_auth_mode = str(cfg.get("proxy_pool_auth_mode", "query") or "").strip().lower() + if proxy_pool_auth_mode not in ("header", "query"): + proxy_pool_auth_mode = "query" + cfg["proxy_pool_auth_mode"] = proxy_pool_auth_mode + cfg["proxy_pool_api_key"] = str(cfg.get("proxy_pool_api_key", "19c0ec43-8f76-4c97-81bc-bcda059eeba4") or "").strip() + try: + cfg["proxy_pool_count"] = max(1, min(int(cfg.get("proxy_pool_count", 1)), 20)) + except (TypeError, ValueError): + cfg["proxy_pool_count"] = 1 + cfg["proxy_pool_country"] = str(cfg.get("proxy_pool_country", "US") or "US").strip().upper() or "US" + return cfg + + +def _pool_relay_url_from_fetch_url(api_url: str) -> str: + raw = str(api_url or "").strip() + if not raw: + return "" + if "://" not in raw: + raw = "https://" + raw + try: + from urllib.parse import urlparse + parsed = urlparse(raw) + scheme = parsed.scheme or "https" + netloc = parsed.netloc + if not netloc: + return "" + return f"{scheme}://{netloc}/api/relay" + except Exception: + return "" + + +def _get_sync_config() -> Dict[str, Any]: + with _config_lock: + return copy.deepcopy(_sync_config) + + +def _set_sync_config(cfg: Dict[str, Any]) -> Dict[str, Any]: + global _sync_config + normalized = _normalize_config(cfg) + with _config_lock: + _write_json_atomic(CONFIG_FILE, normalized) + _sync_config = normalized + return copy.deepcopy(_sync_config) + + +def _save_sync_config(cfg: Dict[str, Any]) -> Dict[str, Any]: + return _set_sync_config(cfg) + + +_sync_config = _normalize_config(_load_sync_config()) + + +def _is_auto_sync_enabled(cfg: Optional[Dict[str, Any]] = None) -> bool: + config = cfg if cfg is not None else _get_sync_config() + return _as_bool(config.get("auto_sync", False), default=False) + + +def _push_refresh_token(base_url: str, bearer: str, refresh_token: str) -> Dict[str, Any]: + """ + 调用 Sub2Api 平台 API 提交单个 refresh_token。 + 返回 {ok: bool, status: int, body: str} + """ + url = base_url.rstrip("/") + "/api/v1/admin/openai/refresh-token" + payload = json.dumps({"refresh_token": refresh_token}).encode("utf-8") + req = urllib.request.Request( + url, + data=payload, + method="POST", + headers={ + "Content-Type": "application/json", + "Authorization": f"Bearer {bearer}", + "Accept": "application/json", + }, + ) + try: + with urllib.request.urlopen(req, timeout=20) as resp: + body = resp.read().decode("utf-8", "replace") + return {"ok": True, "status": resp.status, "body": body} + except urllib.error.HTTPError as exc: + body = exc.read().decode("utf-8", "replace") + return {"ok": False, "status": exc.code, "body": body} + except Exception as e: + return {"ok": False, "status": 0, "body": str(e)} + + +UPLOAD_PLATFORMS = ("cpa", "sub2api") + + +def _extract_uploaded_platforms(token_data: Dict[str, Any]) -> List[str]: + platforms = set() + raw_platforms = token_data.get("uploaded_platforms") + if isinstance(raw_platforms, list): + for p in raw_platforms: + name = str(p).strip().lower() + if name in UPLOAD_PLATFORMS: + platforms.add(name) + if token_data.get("cpa_uploaded") or token_data.get("cpa_synced"): + platforms.add("cpa") + if token_data.get("sub2api_uploaded") or token_data.get("sub2api_synced") or token_data.get("synced"): + platforms.add("sub2api") + return [p for p in UPLOAD_PLATFORMS if p in platforms] + + +def _is_sub2api_uploaded(token_data: Dict[str, Any]) -> bool: + return "sub2api" in _extract_uploaded_platforms(token_data) + + +def _mark_token_uploaded_platform(file_path: str, platform: str) -> bool: + platform_name = str(platform).strip().lower() + if platform_name not in UPLOAD_PLATFORMS: + return False + try: + with open(file_path, "r", encoding="utf-8") as f: + token_data = json.load(f) + if not isinstance(token_data, dict): + return False + + platforms = _extract_uploaded_platforms(token_data) + if platform_name not in platforms: + platforms.append(platform_name) + token_data["uploaded_platforms"] = [p for p in UPLOAD_PLATFORMS if p in set(platforms)] + token_data[f"{platform_name}_uploaded"] = True + token_data[f"{platform_name}_synced"] = True + + if platform_name == "sub2api": + token_data["synced"] = True # 兼容旧前端逻辑 + + uploaded_at = token_data.get("uploaded_at") + if not isinstance(uploaded_at, dict): + uploaded_at = {} + uploaded_at[platform_name] = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + token_data["uploaded_at"] = uploaded_at + + _write_json_atomic(Path(file_path), token_data) + return True + except Exception: + return False + + +# ========================================== +# 统计数据持久化 +# ========================================== + +# STATE_FILE 已从包 __init__.py 导入 + + +def _load_state() -> Dict[str, int]: + if STATE_FILE.exists(): + try: + return json.loads(STATE_FILE.read_text(encoding="utf-8")) + except Exception: + pass + return {"success": 0, "fail": 0} + + +def _save_state(success: int, fail: int) -> None: + try: + _write_json_atomic(STATE_FILE, {"success": success, "fail": fail}) + except Exception: + pass + + +# ========================================== +# 应用初始化 +# ========================================== + +app = FastAPI(title="OpenAI Pool Orchestrator", version=__version__) + +# STATIC_DIR 和 TOKENS_DIR 已从包 __init__.py 导入 +STATIC_DIR.mkdir(exist_ok=True) +os.makedirs(str(TOKENS_DIR), exist_ok=True) + +# ========================================== +# 任务状态管理 +# ========================================== + + +class TaskState: + """全局任务状态,支持多 Worker 运行快照与结构化 SSE 事件。""" + + _WORKER_STEP_DEFINITIONS = { + "check_proxy": "网络检查", + "create_email": "创建邮箱", + "oauth_init": "OAuth 初始化", + "sentinel": "Sentinel Token", + "signup": "提交注册", + "send_otp": "发送验证码", + "wait_otp": "等待验证码", + "verify_otp": "验证 OTP", + "create_account": "创建账户", + "workspace": "选择 Workspace", + "get_token": "获取 Token", + "saved": "保存 Token", + "cpa_upload": "上传 CPA", + "sync": "同步 Sub2Api", + "retry": "等待重试", + "wait": "等待下一轮", + "dedupe": "重复检测", + "runtime": "运行异常", + "auto_stop": "自动停止", + "stopping": "停止中", + "stopped": "已停止", + "mode": "上传策略", + "shutdown": "服务关闭", + } + _REGISTRATION_STEPS = frozenset({ + "check_proxy", "create_email", "oauth_init", "sentinel", + "signup", "send_otp", "wait_otp", "verify_otp", + "create_account", "workspace", "get_token", + }) + + def __init__(self) -> None: + self.status: str = "stopped" + self.stop_event = threading.Event() + self.thread: Optional[threading.Thread] = None + self._worker_threads: Dict[int, threading.Thread] = {} + self._task_lock = threading.RLock() + self._sse_queues: list[tuple[asyncio.AbstractEventLoop, asyncio.Queue]] = [] + self._sse_lock = threading.Lock() + + _s = _load_state() + self.success_count: int = int(_s.get("success", 0) or 0) + self.fail_count: int = int(_s.get("fail", 0) or 0) + self.current_proxy: str = "" + self.worker_count: int = 0 + self.upload_mode: str = "snapshot" + self.target_count: int = 0 + self.run_success_count: int = 0 + self.run_fail_count: int = 0 + self.platform_success_count: Dict[str, int] = {name: 0 for name in UPLOAD_PLATFORMS} + self.platform_fail_count: Dict[str, int] = {name: 0 for name in UPLOAD_PLATFORMS} + self.platform_backlog_count: Dict[str, int] = {name: 0 for name in UPLOAD_PLATFORMS} + self._upload_queues: Dict[str, queue.Queue] = {} + + self.run_id: Optional[str] = None + self.revision: int = 0 + self.created_at: Optional[str] = None + self.started_at: Optional[str] = None + self.finished_at: Optional[str] = None + self.stop_reason: str = "" + self.last_error: str = "" + self.completion_semantics: str = "registration_only" + self._focus_worker_id: Optional[int] = None + self._worker_runtime: Dict[int, Dict[str, Any]] = {} + + def _now_iso(self) -> str: + return datetime.now().isoformat(timespec="seconds") + + def _new_run_id(self) -> str: + return uuid.uuid4().hex[:12] + + def _next_revision_locked(self) -> int: + self.revision += 1 + return self.revision + + def _completion_semantics_locked(self) -> str: + return "requires_postprocess" if _is_auto_sync_enabled() else "registration_only" + + def _empty_worker_runtime_locked(self, worker_id: int, worker_label: Optional[str] = None) -> Dict[str, Any]: + return { + "worker_id": worker_id, + "worker_label": worker_label or f"W{worker_id}", + "status": "starting", + "phase": "prepare", + "attempt": 0, + "mail_provider": "", + "account_email": "", + "current_step": "", + "message": "", + "updated_at": self._now_iso(), + "steps": [], + } + + def _empty_runtime_snapshot_locked(self) -> Dict[str, Any]: + workers = [ + copy.deepcopy(runtime) + for _, runtime in sorted(self._worker_runtime.items(), key=lambda item: item[0]) + ] + return { + "run_id": self.run_id, + "revision": self.revision, + "completion_semantics": self.completion_semantics, + "focus_worker_id": self._focus_worker_id, + "aggregate": self._aggregate_runtime_locked(), + "workers": workers, + } + + def _task_snapshot_locked(self) -> Dict[str, Any]: + return { + "run_id": self.run_id, + "revision": self.revision, + "status": self.status, + "worker_count": self.worker_count, + "upload_mode": self.upload_mode, + "completion_semantics": self.completion_semantics, + "target_count": self.target_count, + "created_at": self.created_at, + "started_at": self.started_at, + "finished_at": self.finished_at, + "stop_reason": self.stop_reason, + "last_error": self.last_error, + "proxy": self.current_proxy, + } + + def _stats_snapshot_locked(self) -> Dict[str, Any]: + platform = {} + for name in UPLOAD_PLATFORMS: + success = int(self.platform_success_count.get(name, 0) or 0) + fail = int(self.platform_fail_count.get(name, 0) or 0) + backlog = int(self.platform_backlog_count.get(name, 0) or 0) + platform[name] = { + "success": success, + "fail": fail, + "backlog": backlog, + "total": success + fail, + } + return { + "lifetime": { + "success": self.success_count, + "fail": self.fail_count, + "total": self.success_count + self.fail_count, + }, + "run": { + "success": self.run_success_count, + "fail": self.run_fail_count, + "total": self.run_success_count + self.run_fail_count, + }, + "platform": platform, + "success": self.success_count, + "fail": self.fail_count, + "total": self.success_count + self.fail_count, + } + + def _status_snapshot_locked(self) -> Dict[str, Any]: + return { + "task": self._task_snapshot_locked(), + "runtime": self._empty_runtime_snapshot_locked(), + "stats": self._stats_snapshot_locked(), + "server_time": self._now_iso(), + } + + def get_status_snapshot(self) -> Dict[str, Any]: + with self._task_lock: + return self._status_snapshot_locked() + + def subscribe(self) -> asyncio.Queue: + loop = asyncio.get_running_loop() + q: asyncio.Queue = asyncio.Queue(maxsize=500) + with self._sse_lock: + self._sse_queues.append((loop, q)) + return q + + def unsubscribe(self, q: asyncio.Queue) -> None: + with self._sse_lock: + self._sse_queues = [(loop, queue_obj) for loop, queue_obj in self._sse_queues if queue_obj is not q] + + def _enqueue_sse_payload(self, payload: Dict[str, Any]) -> None: + with self._sse_lock: + subscribers = list(self._sse_queues) + for loop, q in subscribers: + def _enqueue(target_q: asyncio.Queue = q, data: Dict[str, Any] = payload) -> None: + try: + target_q.put_nowait(copy.deepcopy(data)) + except asyncio.QueueFull: + pass + try: + loop.call_soon_threadsafe(_enqueue) + except RuntimeError: + continue + + def _emit_event_locked(self, event_type: str, payload: Optional[Dict[str, Any]] = None, *, bump_revision: bool = False) -> Dict[str, Any]: + if bump_revision: + self._next_revision_locked() + event_payload: Dict[str, Any] = { + "type": event_type, + "run_id": self.run_id, + "revision": self.revision, + } + if payload: + event_payload.update(payload) + self._enqueue_sse_payload(event_payload) + return event_payload + + def _sync_status_from_workers_locked(self) -> None: + if self.status in {"stopping", "stopped", "finished"}: + return + workers = list(self._worker_runtime.values()) + if not workers: + return + statuses = {str(worker.get("status") or "") for worker in workers} + if any(status == "failed" for status in statuses): + self.status = "failed" + return + if statuses and statuses.issubset({"succeeded", "stopped"}): + self.status = "finished" + return + self.status = "running" + + def _finalize_worker_runtimes_locked(self, final_status: str) -> None: + status = str(final_status or "").strip().lower() + if status != "stopped": + return + updated_at = self._now_iso() + for runtime in self._worker_runtime.values(): + runtime["status"] = "stopped" + runtime["phase"] = "finish" + runtime["current_step"] = "stopped" + runtime["message"] = "任务已停止" + runtime["updated_at"] = updated_at + self._upsert_worker_step_locked( + runtime, + step_id="stopped", + level="info", + message="任务已停止", + updated_at=updated_at, + ) + + def _worker_status_from_step(self, step: str, level: str) -> str: + s = str(step or "").strip().lower() + lv = str(level or "").strip().lower() + if s in {"stopping"}: + return "stopping" + if s in {"stopped", "auto_stop"}: + return "stopped" + if s in {"retry", "wait"}: + return "waiting" + if s == "runtime" or lv == "error": + return "failed" + if s in {"cpa_upload", "sync", "saved"}: + return "postprocessing" + if s in {"start", "dedupe", "mode"}: + return "preparing" + if s in self._REGISTRATION_STEPS: + if s == "get_token" and lv == "success": + return "succeeded" if self.completion_semantics == "registration_only" else "postprocessing" + return "registering" + return "running" if self.status in {"running", "starting"} else self.status + + def _worker_phase_from_step(self, step: str) -> str: + s = str(step or "").strip().lower() + if s in {"start", "dedupe", "mode"}: + return "prepare" + if s in self._REGISTRATION_STEPS: + return "register" + if s in {"saved", "cpa_upload", "sync", "retry", "wait"}: + return "postprocess" + if s in {"stopping", "stopped", "auto_stop", "shutdown"}: + return "finish" + return "prepare" + + def _extract_email_from_event(self, event: Dict[str, Any]) -> str: + direct_email = str(event.get("account_email") or "").strip() + if direct_email: + return direct_email + message = str(event.get("message") or "") + match = re.search(r"([A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,})", message) + return match.group(1) if match else "" + + def _upsert_worker_step_locked(self, runtime: Dict[str, Any], *, step_id: str, level: str, message: str, updated_at: str) -> Dict[str, Any]: + label = self._WORKER_STEP_DEFINITIONS.get(step_id, step_id or "运行步骤") + raw_status = str(level or "info").strip().lower() + if raw_status == "success": + status = "done" + elif raw_status == "error": + status = "error" + elif step_id in {"wait", "retry"}: + status = "active" + else: + status = "active" + steps: List[Dict[str, Any]] = runtime.setdefault("steps", []) + current = None + for item in steps: + if item.get("step_id") == step_id: + current = item + break + if current is None: + current = { + "step_id": step_id, + "id": step_id, + "label": label, + "status": status, + "message": message, + "started_at": updated_at, + "finished_at": updated_at if status in {"done", "error", "skipped"} else None, + "updated_at": updated_at, + } + steps.append(current) + else: + current["label"] = label + current["status"] = status + current["message"] = message + current["updated_at"] = updated_at + current.setdefault("started_at", updated_at) + if status in {"done", "error", "skipped"}: + current["finished_at"] = updated_at + else: + current["finished_at"] = None + return copy.deepcopy(current) + + def _update_runtime_from_event_locked(self, event: Dict[str, Any]) -> Optional[Dict[str, Any]]: + raw_worker_id = event.get("worker_id") + try: + worker_id = int(raw_worker_id) + except (TypeError, ValueError): + return None + + runtime = self._worker_runtime.get(worker_id) + if runtime is None: + runtime = self._empty_worker_runtime_locked(worker_id, str(event.get("worker_label") or f"W{worker_id}")) + self._worker_runtime[worker_id] = runtime + + updated_at = str(event.get("iso_ts") or event.get("updated_at") or self._now_iso()) + runtime["updated_at"] = updated_at + runtime["worker_label"] = str(event.get("worker_label") or runtime.get("worker_label") or f"W{worker_id}") + + attempt = event.get("attempt") + if attempt not in (None, ""): + try: + runtime["attempt"] = int(attempt) + except (TypeError, ValueError): + pass + + mail_provider = str(event.get("mail_provider") or "").strip() + if mail_provider: + runtime["mail_provider"] = mail_provider + + email = self._extract_email_from_event(event) + if email: + runtime["account_email"] = email + runtime["email"] = email + + message = str(event.get("message") or "").strip() + if message: + runtime["message"] = message + + step = str(event.get("step") or "").strip().lower() + level = str(event.get("level") or "info").strip().lower() + step_patch = None + if step: + runtime["current_step"] = step + runtime["phase"] = self._worker_phase_from_step(step) + runtime["status"] = self._worker_status_from_step(step, level) + step_patch = self._upsert_worker_step_locked(runtime, step_id=step, level=level, message=message, updated_at=updated_at) + if step == "start": + runtime["steps"] = [step_patch] + if step in {"stopped", "auto_stop"}: + runtime["status"] = "stopped" + elif step == "runtime" or (level == "error" and step not in {"retry", "wait"}): + runtime["status"] = "failed" + else: + runtime["status"] = "running" if self.status not in {"stopping", "stopped"} else self.status + + if self._focus_worker_id is None or self._focus_worker_id == worker_id: + self._focus_worker_id = worker_id + elif runtime["status"] in {"registering", "postprocessing", "failed", "waiting"}: + self._focus_worker_id = worker_id + + self._sync_status_from_workers_locked() + return { + "worker": copy.deepcopy(runtime), + "step": step_patch, + } + + def _aggregate_runtime_locked(self) -> Dict[str, Any]: + agg: Dict[str, Any] = { + "total": 0, + "starting": 0, + "preparing": 0, + "registering": 0, + "postprocessing": 0, + "waiting": 0, + "stopping": 0, + "stopped": 0, + "failed": 0, + "succeeded": 0, + "last_updated_at": None, + } + for runtime in self._worker_runtime.values(): + agg["total"] += 1 + status = str(runtime.get("status") or "").strip().lower() + if status in agg: + agg[status] += 1 + updated_at = runtime.get("updated_at") + if updated_at and (agg["last_updated_at"] is None or str(updated_at) > str(agg["last_updated_at"])): + agg["last_updated_at"] = updated_at + return agg + + def broadcast(self, event: Dict[str, Any]) -> None: + with self._task_lock: + payload = dict(event) + payload.setdefault("ts", datetime.now().strftime("%H:%M:%S")) + payload.setdefault("iso_ts", self._now_iso()) + event_type = str(payload.get("type") or "").strip() + if event_type: + self._emit_event_locked(event_type, payload, bump_revision=event_type != "heartbeat") + return + + runtime_patch = self._update_runtime_from_event_locked(payload) + self._emit_event_locked( + "log.appended", + { + "log": { + "ts": payload.get("ts", ""), + "level": payload.get("level", "info"), + "message": payload.get("message", ""), + "step": payload.get("step", ""), + "worker_id": payload.get("worker_id"), + "worker_label": payload.get("worker_label"), + } + }, + bump_revision=True, + ) + if runtime_patch: + self._emit_event_locked("worker.updated", {"worker": runtime_patch["worker"]}) + if runtime_patch.get("step"): + self._emit_event_locked( + "worker.step.updated", + { + "worker_id": runtime_patch["worker"].get("worker_id"), + "worker": runtime_patch["worker"], + "step": runtime_patch["step"], + "focus_worker_id": self._focus_worker_id, + }, + ) + self._emit_event_locked("task.updated", {"task": self._task_snapshot_locked()}) + self._emit_event_locked("stats.updated", {"stats": self._stats_snapshot_locked()}) + + def _make_emitter(self) -> EventEmitter: + thread_q: queue.Queue = queue.Queue(maxsize=500) + + def _bridge() -> None: + while True: + try: + event = thread_q.get(timeout=0.2) + if event is None: + break + try: + self.broadcast(event) + except Exception as exc: + try: + print(f"[bridge] log event dropped: {exc}") + except Exception: + pass + except queue.Empty: + if self.stop_event.is_set() and thread_q.empty(): + break + + bridge_thread = threading.Thread(target=_bridge, daemon=True) + bridge_thread.start() + self._bridge_thread = bridge_thread + self._bridge_q = thread_q + return EventEmitter(q=thread_q, cli_mode=True) + + def _stop_bridge(self) -> None: + if hasattr(self, "_bridge_q"): + try: + self._bridge_q.put_nowait(None) + except queue.Full: + pass + + def start_task( + self, + proxy: str, + worker_count: int = 1, + target_count: int = 0, + cpa_target_count: Optional[int] = None, + sub2api_target_count: Optional[int] = None, + ) -> None: + cpa_target = None if cpa_target_count is None else max(0, int(cpa_target_count)) + sub2api_target = None if sub2api_target_count is None else max(0, int(sub2api_target_count)) + config_snapshot = _get_sync_config() + upload_mode = str(config_snapshot.get("upload_mode", "snapshot") or "snapshot").strip().lower() + if upload_mode not in ("snapshot", "decoupled"): + upload_mode = "snapshot" + try: + mail_router = MultiMailRouter(config_snapshot) + except Exception as exc: + raise RuntimeError(str(exc)) from exc + pool_maintainer = _get_pool_maintainer(config_snapshot) + auto_sync_enabled = _is_auto_sync_enabled(config_snapshot) + + with self._task_lock: + if self.status in ("starting", "running", "stopping"): + raise RuntimeError("任务正在运行或停止中") + n = max(1, min(int(worker_count or 1), 10)) + now = self._now_iso() + self.run_id = self._new_run_id() + self.revision = 0 + self.status = "starting" + self.stop_event.clear() + self.current_proxy = proxy + self.worker_count = n + self.upload_mode = upload_mode + self.target_count = max(0, target_count) + self.run_success_count = 0 + self.run_fail_count = 0 + self.platform_success_count = {name: 0 for name in UPLOAD_PLATFORMS} + self.platform_fail_count = {name: 0 for name in UPLOAD_PLATFORMS} + self.platform_backlog_count = {name: 0 for name in UPLOAD_PLATFORMS} + self._upload_queues = {} + self._worker_threads = {} + self._worker_runtime = { + wid: self._empty_worker_runtime_locked(wid) + for wid in range(1, n + 1) + } + self._focus_worker_id = 1 if n > 0 else None + self.created_at = now + self.started_at = now + self.finished_at = None + self.stop_reason = "" + self.last_error = "" + self.completion_semantics = "requires_postprocess" if auto_sync_enabled else "registration_only" + self._emit_event_locked("task.updated", {"task": self._task_snapshot_locked()}, bump_revision=True) + self._emit_event_locked("snapshot", {"snapshot": self._status_snapshot_locked()}) + + emitter = self._make_emitter() + emitter.info( + f"上传策略: {'串行补平台(先CPA后Sub2Api)' if upload_mode == 'snapshot' else '双平台同传(单账号双上传)'}", + step="mode", + ) + + + upload_remaining: Dict[str, Optional[int]] = { + "cpa": cpa_target, + "sub2api": sub2api_target, + } + snapshot_strict_serial = ( + upload_mode == "snapshot" + and cpa_target is not None + and sub2api_target is not None + ) + token_states: Dict[str, Dict[str, Any]] = {} + token_states_lock = threading.RLock() + seen_runtime_identities: set[str] = _load_local_token_identity_keys() + seen_runtime_identities_lock = threading.RLock() + upload_queues: Dict[str, queue.Queue] = {} + upload_workers: Dict[str, threading.Thread] = {} + producers_done = threading.Event() + + def _reserve_upload_slot(platform: str) -> bool: + with self._task_lock: + remain = upload_remaining.get(platform) + if remain is None: + return True + if remain <= 0: + return False + upload_remaining[platform] = remain - 1 + return True + + def _release_upload_slot(platform: str) -> None: + with self._task_lock: + remain = upload_remaining.get(platform) + if remain is not None: + upload_remaining[platform] = remain + 1 + + def _decoupled_slots_exhausted() -> bool: + """仅在双平台同传 + 有限配额场景下判断是否已无可用上传槽位。""" + if upload_mode != "decoupled": + return False + with self._task_lock: + finite_remains = [ + remain + for remain in upload_remaining.values() + if remain is not None + ] + return bool(finite_remains) and all(remain <= 0 for remain in finite_remains) + + def _reserve_snapshot_serial_platform() -> Optional[str]: + with self._task_lock: + cpa_remain = upload_remaining.get("cpa") + if cpa_remain is not None and cpa_remain > 0: + upload_remaining["cpa"] = cpa_remain - 1 + return "cpa" + sub2api_remain = upload_remaining.get("sub2api") + if sub2api_remain is not None and sub2api_remain > 0: + upload_remaining["sub2api"] = sub2api_remain - 1 + return "sub2api" + return None + + def _record_platform_result(platform: str, ok: bool) -> None: + if platform not in UPLOAD_PLATFORMS: + return + with self._task_lock: + if ok: + self.platform_success_count[platform] = self.platform_success_count.get(platform, 0) + 1 + else: + self.platform_fail_count[platform] = self.platform_fail_count.get(platform, 0) + 1 + + def _register_runtime_identity(email: str, refresh_token: str) -> bool: + keys = _sub2api_identity_keys(email=email, refresh_token=refresh_token) + if not keys: + return True + with seen_runtime_identities_lock: + for key in keys: + if key in seen_runtime_identities: + return False + seen_runtime_identities.update(keys) + return True + + def _refresh_backlog() -> None: + with self._task_lock: + if upload_mode != "decoupled": + self.platform_backlog_count = {name: 0 for name in UPLOAD_PLATFORMS} + return + self.platform_backlog_count = { + platform: q.qsize() + for platform, q in upload_queues.items() + } + + def _apply_final_result(email: str, prefix: str, ok: bool) -> None: + if ok: + with self._task_lock: + self.success_count += 1 + self.run_success_count += 1 + _save_state(self.success_count, self.fail_count) + should_stop = self.target_count > 0 and self.run_success_count >= self.target_count + if should_stop: + emitter.success( + f"{prefix}本轮已达目标 {self.target_count} 个,自动停止", + step="auto_stop", + ) + self.stop_event.set() + else: + with self._task_lock: + self.fail_count += 1 + self.run_fail_count += 1 + _save_state(self.success_count, self.fail_count) + emitter.error(f"{prefix}平台上传未完成,本次不计入成功: {email}", step="retry") + + def _auto_sync(file_name: str, email: str, em: "EventEmitter") -> bool: + cfg = config_snapshot + if not _is_auto_sync_enabled(cfg): + return True + base_url = cfg.get("base_url", "").strip() + bearer = cfg.get("bearer_token", "").strip() + if not base_url or not bearer: + em.error("自动同步配置缺少平台地址或 Token,请先保存配置", step="sync") + return False + + em.info(f"正在自动同步 {email}...", step="sync") + fpath = os.path.join(TOKENS_DIR, file_name) + try: + with open(fpath, "r", encoding="utf-8") as f: + token_data = json.load(f) + except Exception as e: + em.error(f"自动同步异常: 读取本地 Token 失败: {e}", step="sync") + return False + + last_status = 0 + last_body = "" + for attempt in range(3): + try: + result = _push_account_api_with_dedupe( + base_url=base_url, + bearer=bearer, + email=email, + token_data=token_data, + check_before=(attempt == 0), + check_after=True, + ) + last_status = int(result.get("status") or 0) + last_body = str(result.get("body") or "") + if result.get("ok"): + if not _mark_token_uploaded_platform(fpath, "sub2api"): + em.warn(f"自动同步成功但本地标记失败: {email}", step="sync") + reason = str(result.get("reason") or "") + if reason == "updated_existing_before_create": + em.success( + f"自动同步命中已存在账号并更新凭据: {email} (id={result.get('existing_id', '-')})", + step="sync", + ) + elif reason == "exists_before_create_update_failed": + em.warn( + f"自动同步命中已存在账号但更新失败,保持远端现状: {email} " + f"(id={result.get('existing_id', '-')}, status={result.get('update_status', '-')}) " + f"{str(result.get('update_body') or '')[:120]}", + step="sync", + ) + elif result.get("skipped"): + em.success(f"自动同步成功: {email}", step="sync") + else: + em.success(f"自动同步成功: {email}", step="sync") + return True + except Exception as e: + last_status = 0 + last_body = str(e) + if attempt < 2: + time.sleep(2 ** attempt) + + em.error(f"自动同步失败({last_status}): {last_body[:120]}", step="sync") + return False + + def _upload_to_cpa(file_name: str, file_path: str, token_json: str, email: str, prefix: str) -> bool: + if not pool_maintainer: + return True + try: + td = json.loads(token_json) + cpa_ok = pool_maintainer.upload_token(file_name, td, proxy=proxy or "") + if cpa_ok: + if not _mark_token_uploaded_platform(file_path, "cpa"): + emitter.warn(f"{prefix}CPA 上传成功但本地标记失败: {email}", step="cpa_upload") + emitter.success(f"{prefix}CPA 上传成功: {email}", step="cpa_upload") + else: + emitter.error(f"{prefix}CPA 上传失败: {email}", step="cpa_upload") + return cpa_ok + except Exception as ex: + emitter.error(f"{prefix}CPA 上传异常: {ex}", step="cpa_upload") + return False + + def _upload_to_sub2api(file_name: str, email: str, refresh_token: str, prefix: str) -> bool: + if not auto_sync_enabled: + return True + if not refresh_token: + emitter.error(f"{prefix}缺少 refresh_token,无法自动同步: {email}", step="sync") + return False + return _auto_sync(file_name, email, emitter) + + def _register_decoupled_token( + token_key: str, + email: str, + prefix: str, + required_platforms: set[str], + failed_platforms: set[str], + ) -> None: + final_ok: Optional[bool] = None + no_required_platforms = False + with token_states_lock: + token_states[token_key] = { + "email": email, + "prefix": prefix, + "required": set(required_platforms), + "done": set(), + "failed": set(failed_platforms), + "finalized": False, + } + state = token_states[token_key] + if state["failed"]: + state["finalized"] = True + token_states.pop(token_key, None) + final_ok = False + elif not state["required"]: + state["finalized"] = True + token_states.pop(token_key, None) + no_required_platforms = True + if final_ok is not None: + _apply_final_result(email, prefix, final_ok) + return + if no_required_platforms: + # 有限配额耗尽后,后续注册不应继续计入成功。 + if _decoupled_slots_exhausted(): + emitter.info( + f"{prefix}平台目标已满足,跳过本次上传且不计成功: {email}", + step="auto_stop", + ) + self.stop_event.set() + return + # 兼容手动启动且无上传平台/无限配额场景:保留“注册成功”计数行为。 + _apply_final_result(email, prefix, True) + + def _complete_decoupled_platform(token_key: str, platform: str, ok: bool) -> None: + final_ok: Optional[bool] = None + email = "unknown" + prefix = "" + with token_states_lock: + state = token_states.get(token_key) + if not state or state.get("finalized"): + return + if ok: + state["done"].add(platform) + else: + state["failed"].add(platform) + email = state.get("email", "unknown") + prefix = state.get("prefix", "") + if state["failed"]: + state["finalized"] = True + token_states.pop(token_key, None) + final_ok = False + elif state["required"].issubset(state["done"]): + state["finalized"] = True + token_states.pop(token_key, None) + final_ok = True + if final_ok is not None: + _apply_final_result(email, prefix, final_ok) + + def _enqueue_upload_job(platform: str, job: Dict[str, Any], prefix: str) -> None: + q = upload_queues.get(platform) + if not q: + _release_upload_slot(platform) + _complete_decoupled_platform(job["token_key"], platform, False) + return + try: + q.put_nowait(job) + _refresh_backlog() + except queue.Full: + emitter.error(f"{prefix}{platform.upper()} 上传队列已满,跳过: {job.get('email', 'unknown')}", step="sync") + _release_upload_slot(platform) + _complete_decoupled_platform(job["token_key"], platform, False) + + def _upload_worker_loop(platform: str) -> None: + q = upload_queues[platform] + while True: + if producers_done.is_set() and q.empty(): + break + try: + job = q.get(timeout=0.3) + except queue.Empty: + _refresh_backlog() + continue + + _refresh_backlog() + ok = False + if platform == "cpa": + ok = _upload_to_cpa( + file_name=job["file_name"], + file_path=job["file_path"], + token_json=job["token_json"], + email=job["email"], + prefix=job.get("prefix", ""), + ) + elif platform == "sub2api": + ok = _upload_to_sub2api( + file_name=job["file_name"], + email=job["email"], + refresh_token=job.get("refresh_token", ""), + prefix=job.get("prefix", ""), + ) + _record_platform_result(platform, ok) + if not ok: + _release_upload_slot(platform) + _complete_decoupled_platform(job["token_key"], platform, ok) + q.task_done() + _refresh_backlog() + + def _worker_loop(worker_id: int) -> None: + worker_label = f"W{worker_id}" + prefix = f"[{worker_label}] " if n > 1 else "" + worker_emitter = emitter.bind(worker_id=worker_id, worker_label=worker_label) + count = 0 + while not self.stop_event.is_set(): + if _decoupled_slots_exhausted(): + worker_emitter.info(f"{prefix}双平台目标已满足,停止新增注册", step="auto_stop") + self.stop_event.set() + break + count += 1 + provider_name, provider = mail_router.next_provider() + attempt_emitter = worker_emitter.bind(mail_provider=provider_name) + attempt_emitter.info( + f"{prefix}>>> 第 {count} 次注册 (邮箱: {provider_name}) <<<", + step="start", + attempt=count, + ) + try: + token_json = run( + proxy=proxy or None, + emitter=attempt_emitter, + stop_event=self.stop_event, + mail_provider=provider, + proxy_pool_config={ + "enabled": bool(config_snapshot.get("proxy_pool_enabled", False)), + "api_url": str(config_snapshot.get("proxy_pool_api_url", "")).strip(), + "auth_mode": str(config_snapshot.get("proxy_pool_auth_mode", "query")).strip().lower(), + "api_key": str(config_snapshot.get("proxy_pool_api_key", "")).strip(), + "count": config_snapshot.get("proxy_pool_count", 1), + "country": str(config_snapshot.get("proxy_pool_country", "US") or "US").strip().upper(), + }, + ) + + if self.stop_event.is_set() and not token_json: + break + + if token_json: + mail_router.report_success(provider_name) + try: + t_data = json.loads(token_json) + fname_email = t_data.get("email", "unknown").replace("@", "_") + refresh_token = str(t_data.get("refresh_token", "") or "").strip() + email = str(t_data.get("email", "unknown") or "unknown").strip() + except Exception: + fname_email = "unknown" + refresh_token = "" + email = "unknown" + + if not _register_runtime_identity(email, refresh_token): + attempt_emitter.warn( + f"{prefix}检测到重复账号(同邮箱/refresh_token),已跳过: {email}", + step="dedupe", + account_email=email, + ) + continue + + file_name = f"token_{fname_email}_{time.time_ns()}.json" + file_path = os.path.join(TOKENS_DIR, file_name) + with open(file_path, "w", encoding="utf-8") as f: + f.write(token_json) + + attempt_emitter.success(f"{prefix}Token 已保存: {file_name}", step="saved", account_email=email) + self.broadcast({ + "ts": datetime.now().strftime("%H:%M:%S"), + "level": "token_saved", + "message": file_name, + "step": "saved", + "worker_id": worker_id, + "worker_label": worker_label, + "mail_provider": provider_name, + "attempt": count, + "account_email": email, + }) + + if upload_mode == "snapshot": + if snapshot_strict_serial: + selected_platform = _reserve_snapshot_serial_platform() + if selected_platform == "cpa": + attempt_emitter.info(f"{prefix}串行模式:本次仅上传 CPA -> {email}", step="cpa_upload", account_email=email) + cpa_ok = _upload_to_cpa(file_name, file_path, token_json, email, prefix) if pool_maintainer else True + _record_platform_result("cpa", cpa_ok) + if not cpa_ok: + _release_upload_slot("cpa") + _apply_final_result(email, prefix, cpa_ok) + elif selected_platform == "sub2api": + attempt_emitter.info(f"{prefix}串行模式:本次仅上传 Sub2Api -> {email}", step="sync", account_email=email) + sub2api_ok = _upload_to_sub2api(file_name, email, refresh_token, prefix) if auto_sync_enabled else True + _record_platform_result("sub2api", sub2api_ok) + if not sub2api_ok: + _release_upload_slot("sub2api") + _apply_final_result(email, prefix, sub2api_ok) + else: + attempt_emitter.info(f"{prefix}串行模式目标已满足,停止新增上传: {email}", step="auto_stop", account_email=email) + self.stop_event.set() + else: + cpa_ok = True + cpa_required = False + if pool_maintainer: + cpa_required = _reserve_upload_slot("cpa") + if pool_maintainer and not cpa_required: + attempt_emitter.info(f"{prefix}CPA 已达目标阈值,跳过上传: {email}", step="cpa_upload", account_email=email) + if pool_maintainer and cpa_required: + cpa_ok = _upload_to_cpa(file_name, file_path, token_json, email, prefix) + _record_platform_result("cpa", cpa_ok) + if not cpa_ok: + _release_upload_slot("cpa") + + sub2api_ok = True + sub2api_required = False + if auto_sync_enabled: + sub2api_required = _reserve_upload_slot("sub2api") + if auto_sync_enabled and not sub2api_required: + attempt_emitter.info(f"{prefix}Sub2Api 已达目标阈值,跳过同步: {email}", step="sync", account_email=email) + if auto_sync_enabled and sub2api_required: + sub2api_ok = _upload_to_sub2api(file_name, email, refresh_token, prefix) + _record_platform_result("sub2api", sub2api_ok) + if not sub2api_ok: + _release_upload_slot("sub2api") + + _apply_final_result(email, prefix, cpa_ok and sub2api_ok) + else: + required_platforms: set[str] = set() + failed_platforms: set[str] = set() + + if pool_maintainer: + if _reserve_upload_slot("cpa"): + required_platforms.add("cpa") + else: + attempt_emitter.info(f"{prefix}CPA 已达目标阈值,跳过上传: {email}", step="cpa_upload", account_email=email) + + if auto_sync_enabled: + if _reserve_upload_slot("sub2api"): + if refresh_token: + required_platforms.add("sub2api") + else: + failed_platforms.add("sub2api") + _release_upload_slot("sub2api") + attempt_emitter.error(f"{prefix}缺少 refresh_token,无法自动同步: {email}", step="sync", account_email=email) + else: + attempt_emitter.info(f"{prefix}Sub2Api 已达目标阈值,跳过同步: {email}", step="sync", account_email=email) + + token_key = file_name + _register_decoupled_token(token_key, email, prefix, required_platforms, failed_platforms) + + base_job = { + "token_key": token_key, + "file_name": file_name, + "file_path": file_path, + "token_json": token_json, + "email": email, + "refresh_token": refresh_token, + "prefix": prefix, + } + if "cpa" in required_platforms: + _enqueue_upload_job("cpa", base_job, prefix) + if "sub2api" in required_platforms: + _enqueue_upload_job("sub2api", base_job, prefix) + else: + mail_router.report_failure(provider_name) + with self._task_lock: + self.fail_count += 1 + self.run_fail_count += 1 + self.last_error = f"注册失败: worker={worker_id}" + _save_state(self.success_count, self.fail_count) + self.status = "running" + attempt_emitter.error(f"{prefix}本次注册失败,稍后重试...", step="retry") + + except Exception as e: + mail_router.report_failure(provider_name) + with self._task_lock: + self.fail_count += 1 + self.run_fail_count += 1 + self.last_error = str(e) + _save_state(self.success_count, self.fail_count) + attempt_emitter.error(f"{prefix}发生未捕获异常: {e}", step="runtime") + + if self.stop_event.is_set(): + break + + wait = random.randint(5, 30) + attempt_emitter.info(f"{prefix}休息 {wait} 秒后继续...", step="wait") + self.stop_event.wait(wait) + + if upload_mode == "decoupled": + upload_queues = { + platform: queue.Queue(maxsize=2000) + for platform in UPLOAD_PLATFORMS + } + with self._task_lock: + self._upload_queues = upload_queues + _refresh_backlog() + for platform in UPLOAD_PLATFORMS: + t = threading.Thread(target=_upload_worker_loop, args=(platform,), daemon=True) + upload_workers[platform] = t + t.start() + + def _monitor() -> None: + with self._task_lock: + workers = list(self._worker_threads.values()) + for t in workers: + t.join() + if upload_mode == "decoupled": + producers_done.set() + for ut in upload_workers.values(): + ut.join() + stale_results: List[Dict[str, Any]] = [] + with token_states_lock: + for token_key in list(token_states.keys()): + state = token_states.pop(token_key, None) + if state and not state.get("finalized"): + stale_results.append(state) + for state in stale_results: + _apply_final_result(state.get("email", "unknown"), state.get("prefix", ""), False) + with self._task_lock: + self._upload_queues = {} + self.platform_backlog_count = {name: 0 for name in UPLOAD_PLATFORMS} + self._emit_event_locked("stats.updated", {"stats": self._stats_snapshot_locked()}, bump_revision=True) + + emitter.info("所有Worker已停止", step="stopped") + self._stop_bridge() + with self._task_lock: + self._worker_threads.clear() + self.worker_count = 0 + self.finished_at = self._now_iso() + if self.status == "stopping": + self.status = "stopped" + self.stop_reason = self.stop_reason or "manual_stop" + elif self.status == "failed": + pass + elif self.run_fail_count > 0 and self.run_success_count == 0: + self.status = "failed" + self.stop_reason = self.stop_reason or "run_failed" + else: + self.status = "finished" + if self.status == "stopped": + self._finalize_worker_runtimes_locked("stopped") + self._sync_status_from_workers_locked() + self._emit_event_locked("task.finished", {"task": self._task_snapshot_locked()}, bump_revision=True) + self._emit_event_locked("snapshot", {"snapshot": self._status_snapshot_locked()}) + + for wid in range(1, n + 1): + t = threading.Thread(target=_worker_loop, args=(wid,), daemon=True) + with self._task_lock: + self._worker_threads[wid] = t + t.start() + + with self._task_lock: + self.status = "running" + self._emit_event_locked("task.updated", {"task": self._task_snapshot_locked()}, bump_revision=True) + self._emit_event_locked("snapshot", {"snapshot": self._status_snapshot_locked()}) + + self.thread = threading.Thread(target=_monitor, daemon=True) + self.thread.start() + + def stop_task(self) -> None: + with self._task_lock: + if self.status in {"starting", "running", "failed"}: + self.status = "stopping" + self.stop_reason = "manual_stop" + self.stop_event.set() + self._emit_event_locked("task.updated", {"task": self._task_snapshot_locked()}, bump_revision=True) + self._emit_event_locked("snapshot", {"snapshot": self._status_snapshot_locked()}) + else: + return + self.broadcast({ + "ts": datetime.now().strftime("%H:%M:%S"), + "level": "info", + "message": "收到停止请求,等待当前注册流程收尾...", + "step": "stopping", + }) + + +_state = TaskState() + + +def request_service_shutdown() -> None: + """供外部启动器调用,通知服务进入收尾停止流程。""" + _service_shutdown_event.set() + + try: + _state.broadcast({ + "level": "info", + "message": "收到服务关闭请求,正在停止任务与后台维护线程...", + "step": "shutdown", + }) + except Exception: + pass + + try: + _state.stop_task() + except Exception: + pass + + try: + _stop_auto_maintain() + except Exception: + pass + + try: + _stop_sub2api_auto_maintain() + except Exception: + pass + + +# 自动维护后台任务 +_auto_maintain_thread: Optional[threading.Thread] = None +_auto_maintain_stop: Optional[threading.Event] = None +_auto_maintain_ctl_lock = threading.Lock() +_pool_maintain_lock = threading.Lock() + + +def _get_pool_maintainer(cfg: Optional[Dict[str, Any]] = None) -> Optional[PoolMaintainer]: + cfg = cfg or _get_sync_config() + base_url = str(cfg.get("cpa_base_url", "")).strip() + token = str(cfg.get("cpa_token", "")).strip() + if not base_url or not token: + return None + return PoolMaintainer( + cpa_base_url=base_url, + cpa_token=token, + min_candidates=int(cfg.get("min_candidates", 800)), + used_percent_threshold=int(cfg.get("used_percent_threshold", 95)), + ) + + +def _get_sub2api_maintainer(cfg: Optional[Dict[str, Any]] = None) -> Optional[Sub2ApiMaintainer]: + cfg = cfg or _get_sync_config() + base_url = str(cfg.get("base_url", "")).strip() + bearer = str(cfg.get("bearer_token", "")).strip() + email = str(cfg.get("email", "")).strip() + password = str(cfg.get("password", "")).strip() + if not base_url: + return None + if not bearer and not (email and password): + return None + return Sub2ApiMaintainer( + base_url=base_url, + bearer_token=bearer, + min_candidates=int(cfg.get("sub2api_min_candidates", 200)), + email=email, + password=password, + ) + + +# ========================================== +# API 路由 +# ========================================== + + +class StartRequest(BaseModel): + proxy: str = "" + worker_count: int = 1 + + +class ProxyCheckRequest(BaseModel): + proxy: str = "" + + +class ProxyPoolTestRequest(BaseModel): + enabled: bool = True + api_url: str = "https://zenproxy.top/api/fetch" + auth_mode: str = "query" # "header" | "query" + api_key: str = "" + count: int = 1 + country: str = "US" + + +class ProxyPoolConfigRequest(BaseModel): + proxy_pool_enabled: bool = True + proxy_pool_api_url: str = "https://zenproxy.top/api/fetch" + proxy_pool_auth_mode: str = "query" # "header" | "query" + proxy_pool_api_key: str = "" + proxy_pool_count: int = 1 + proxy_pool_country: str = "US" + + +class ProxySaveRequest(BaseModel): + proxy: str = "" + auto_register: bool = False + + +class SyncConfigRequest(BaseModel): + base_url: str # Sub2Api 平台地址 + bearer_token: str = "" # 管理员 JWT(可选) + email: str = "" # 管理员邮箱 + password: str = "" # 管理员密码 + account_name: str = "AutoReg" + auto_sync: bool = True + upload_mode: str = "snapshot" # "snapshot" | "decoupled" + sub2api_min_candidates: int = 200 + sub2api_auto_maintain: bool = False + sub2api_maintain_interval_minutes: int = 30 + sub2api_maintain_actions: Dict[str, bool] = Field(default_factory=dict) + multithread: bool = False + thread_count: int = 3 + auto_register: bool = False + + +class SyncNowRequest(BaseModel): + filenames: List[str] = [] # 空列表 = 同步全部 + + +class UploadModeRequest(BaseModel): + upload_mode: str = "snapshot" # "snapshot" | "decoupled" + + +@app.get("/", response_class=HTMLResponse) +async def index() -> HTMLResponse: + html_path = STATIC_DIR / "index.html" + if html_path.exists(): + return HTMLResponse(content=html_path.read_text(encoding="utf-8")) + return HTMLResponse("

前端文件未找到

", status_code=404) + + +@app.post("/api/start") +async def api_start(req: StartRequest) -> Dict[str, Any]: + try: + _state.start_task(req.proxy, req.worker_count) + except RuntimeError as e: + raise HTTPException(status_code=409, detail=str(e)) + snapshot = _state.get_status_snapshot() + return { + "run_id": snapshot["task"].get("run_id"), + "task": snapshot["task"], + "runtime": snapshot["runtime"], + "stats": snapshot["stats"], + "server_time": snapshot["server_time"], + } + + +@app.post("/api/stop") +async def api_stop() -> Dict[str, Any]: + if _state.status in {"stopped", "finished", "failed"} and not _state.run_id: + raise HTTPException(status_code=409, detail="没有正在运行的任务") + _state.stop_task() + return _state.get_status_snapshot() + + +@app.post("/api/proxy/save") +async def api_save_proxy(req: ProxySaveRequest) -> Dict[str, str]: + cfg = _get_sync_config() + cfg["proxy"] = req.proxy.strip() + cfg["auto_register"] = req.auto_register + _save_sync_config(cfg) + return {"status": "saved"} + + +@app.get("/api/proxy") +async def api_get_proxy() -> Dict[str, Any]: + cfg = _get_sync_config() + return { + "proxy": cfg.get("proxy", ""), + "auto_register": cfg.get("auto_register", False), + } + + +@app.get("/api/status") +async def api_status() -> Dict[str, Any]: + return _state.get_status_snapshot() + + +@app.get("/api/tokens") +async def api_tokens() -> Dict[str, Any]: + def _read_tokens(): + tokens = [] + if os.path.isdir(TOKENS_DIR): + import re + def _sort_key(f): + m = re.search(r'_(\d{10,})\.json$', f) + return int(m.group(1)) if m else 0 + + all_files = [f for f in os.listdir(TOKENS_DIR) if f.endswith(".json")] + all_files.sort(key=_sort_key, reverse=True) + for fname in all_files: + fpath = os.path.join(TOKENS_DIR, fname) + try: + with open(fpath, "r", encoding="utf-8") as f: + content_raw = json.load(f) + content = content_raw if isinstance(content_raw, dict) else {} + uploaded_platforms = _extract_uploaded_platforms(content) + tokens.append( + { + "filename": fname, + "email": content.get("email", ""), + "expired": content.get("expired", ""), + "uploaded_platforms": uploaded_platforms, + "content": content, + } + ) + except Exception: + pass + return tokens + + tokens = await run_in_threadpool(_read_tokens) + return {"tokens": tokens} + + +@app.delete("/api/tokens/{filename}") +async def api_delete_token(filename: str) -> Dict[str, str]: + # 安全过滤:防止路径穿越 + if "/" in filename or "\\" in filename or ".." in filename: + raise HTTPException(status_code=400, detail="非法文件名") + fpath = os.path.join(TOKENS_DIR, filename) + if not os.path.isfile(fpath): + raise HTTPException(status_code=404, detail="文件不存在") + os.remove(fpath) + return {"status": "deleted"} + + +@app.get("/api/sync-config") +async def api_get_sync_config() -> Dict[str, Any]: + """获取当前同步配置(脱敏)""" + cfg = _get_sync_config() + cfg["password"] = "" # 不回传密码 + token = cfg.get("bearer_token", "") + cfg["bearer_token_preview"] = token[:12] + "..." if len(token) > 12 else (token or "") + cfg["bearer_token"] = "" # 不回传完整 token + # 脱敏 cpa_token + cpa_token = str(cfg.get("cpa_token", "")) + cfg["cpa_token_preview"] = (cpa_token[:12] + "...") if len(cpa_token) > 12 else (cpa_token or "") + cfg["cpa_token"] = "" + proxy_pool_api_key = str(cfg.get("proxy_pool_api_key", "")) + cfg["proxy_pool_api_key_preview"] = ( + (proxy_pool_api_key[:8] + "...") if len(proxy_pool_api_key) > 8 else (proxy_pool_api_key or "") + ) + cfg["proxy_pool_api_key"] = "" + # 脱敏 mail_provider_configs + raw_configs = cfg.get("mail_provider_configs") or {} + safe_configs: Dict[str, Dict] = {} + for pname, pcfg in raw_configs.items(): + if not isinstance(pcfg, dict): + continue + sc = dict(pcfg) + for secret_key in ("bearer_token", "api_key", "admin_password"): + val = str(sc.get(secret_key, "")) + if val: + sc[f"{secret_key}_preview"] = (val[:8] + "...") if len(val) > 8 else val + sc.pop(secret_key, None) + safe_configs[pname] = sc + cfg["mail_provider_configs"] = safe_configs + cfg.setdefault("sub2api_min_candidates", 200) + cfg.setdefault("sub2api_auto_maintain", False) + cfg.setdefault("sub2api_maintain_interval_minutes", 30) + cfg.setdefault("upload_mode", "snapshot") + cfg.setdefault("multithread", False) + cfg.setdefault("thread_count", 3) + cfg.setdefault("proxy_pool_enabled", True) + cfg.setdefault("proxy_pool_api_url", "https://zenproxy.top/api/fetch") + cfg.setdefault("proxy_pool_auth_mode", "query") + cfg.setdefault("proxy_pool_count", 1) + cfg.setdefault("proxy_pool_country", "US") + cfg["auto_sync"] = _is_auto_sync_enabled(cfg) + return cfg + + +@app.get("/api/proxy-pool/config") +async def api_get_proxy_pool_config() -> Dict[str, Any]: + cfg = _get_sync_config() + api_url = str(cfg.get("proxy_pool_api_url", "https://zenproxy.top/api/fetch") or "").strip() + if not api_url: + api_url = "https://zenproxy.top/api/fetch" + auth_mode = str(cfg.get("proxy_pool_auth_mode", "query") or "").strip().lower() + if auth_mode not in ("header", "query"): + auth_mode = "query" + try: + count = max(1, min(int(cfg.get("proxy_pool_count", 1) or 1), 20)) + except (TypeError, ValueError): + count = 1 + country = str(cfg.get("proxy_pool_country", "US") or "US").strip().upper() or "US" + api_key = str(cfg.get("proxy_pool_api_key", "") or "").strip() + return { + "proxy_pool_enabled": bool(cfg.get("proxy_pool_enabled", True)), + "proxy_pool_api_url": api_url, + "proxy_pool_auth_mode": auth_mode, + "proxy_pool_api_key": "", + "proxy_pool_api_key_preview": (api_key[:8] + "...") if len(api_key) > 8 else (api_key or ""), + "proxy_pool_count": count, + "proxy_pool_country": country, + } + + +@app.post("/api/proxy-pool/config") +async def api_set_proxy_pool_config(req: ProxyPoolConfigRequest) -> Dict[str, Any]: + cfg = _get_sync_config() + proxy_pool_auth_mode = str(req.proxy_pool_auth_mode or "query").strip().lower() + if proxy_pool_auth_mode not in ("header", "query"): + proxy_pool_auth_mode = "query" + + proxy_pool_api_url = str(req.proxy_pool_api_url or "https://zenproxy.top/api/fetch").strip() + if not proxy_pool_api_url: + proxy_pool_api_url = "https://zenproxy.top/api/fetch" + + proxy_pool_api_key = req.proxy_pool_api_key.strip() if req.proxy_pool_api_key else "" + if not proxy_pool_api_key: + proxy_pool_api_key = str(cfg.get("proxy_pool_api_key", "") or "").strip() + + try: + proxy_pool_count = max(1, min(int(req.proxy_pool_count), 20)) + except (TypeError, ValueError): + proxy_pool_count = 1 + proxy_pool_country = str(req.proxy_pool_country or "US").strip().upper() or "US" + + cfg.update({ + "proxy_pool_enabled": bool(req.proxy_pool_enabled), + "proxy_pool_api_url": proxy_pool_api_url, + "proxy_pool_auth_mode": proxy_pool_auth_mode, + "proxy_pool_api_key": proxy_pool_api_key, + "proxy_pool_count": proxy_pool_count, + "proxy_pool_country": proxy_pool_country, + }) + _save_sync_config(cfg) + return {"status": "saved"} + + +@app.post("/api/upload-mode") +async def api_set_upload_mode(req: UploadModeRequest) -> Dict[str, Any]: + upload_mode = str(req.upload_mode or "snapshot").strip().lower() + if upload_mode not in ("snapshot", "decoupled"): + raise HTTPException(status_code=400, detail="upload_mode 仅支持 snapshot / decoupled") + cfg = _get_sync_config() + cfg["upload_mode"] = upload_mode + _save_sync_config(cfg) + # 空闲状态下同步到内存状态,便于前端立即看到当前策略 + with _state._task_lock: + if _state.status == "idle": + _state.upload_mode = upload_mode + return {"status": "saved", "upload_mode": upload_mode} + + +def _verify_sub2api_login(base_url: str, email: str, password: str) -> Dict[str, Any]: + """通过 HTTP API 验证 Sub2Api 平台登录凭据是否正确""" + from curl_cffi import requests as cffi_req + + # 自动补全协议(优先 https://) + url = base_url.strip() + if not url.startswith(("http://", "https://")): + url = "https://" + url + + login_url = url.rstrip("/") + "/api/v1/auth/login" + try: + resp = cffi_req.post( + login_url, + json={"email": email, "password": password}, + impersonate="chrome", + timeout=15, + ) + raw_body = resp.text + if resp.status_code != 200: + try: + err_body = json.loads(raw_body) + err_msg = err_body.get("message") or err_body.get("error") or raw_body[:200] + except json.JSONDecodeError: + err_msg = raw_body[:200] + return {"ok": False, "error": f"登录失败(HTTP {resp.status_code}): {err_msg}"} + try: + body = json.loads(raw_body) + except json.JSONDecodeError: + return {"ok": False, "error": f"服务器返回非 JSON 格式: {raw_body[:200]}"} + + token = ( + body.get("token") + or body.get("access_token") + or (body.get("data") or {}).get("token") + or (body.get("data") or {}).get("access_token") + or "" + ) + return {"ok": True, "token": token} + except Exception as e: + return {"ok": False, "error": f"请求异常: {e}"} + + +def _verify_sub2api_token(base_url: str, bearer_token: str) -> Dict[str, Any]: + from curl_cffi import requests as cffi_req + + url = base_url.strip() + if not url.startswith(("http://", "https://")): + url = "https://" + url + + verify_url = url.rstrip("/") + "/api/v1/admin/dashboard/stats" + try: + resp = cffi_req.get( + verify_url, + headers={ + "Authorization": f"Bearer {bearer_token}", + "Accept": "application/json", + }, + params={"timezone": "Asia/Shanghai"}, + impersonate="chrome", + timeout=15, + ) + if resp.status_code != 200: + return {"ok": False, "error": f"Bearer Token 验证失败: HTTP {resp.status_code}"} + return {"ok": True} + except Exception as e: + return {"ok": False, "error": f"Bearer Token 验证异常: {e}"} + + +@app.post("/api/sync-config") +async def api_set_sync_config(req: SyncConfigRequest) -> Dict[str, Any]: + """保存同步配置(先验证登录凭据)""" + cfg = _get_sync_config() + new_base_url = req.base_url.strip() + if new_base_url and not new_base_url.startswith(("http://", "https://")): + new_base_url = "https://" + new_base_url + new_email = req.email.strip() or str(cfg.get("email", "") or "").strip() + new_password = req.password.strip() if req.password else str(cfg.get("password", "") or "").strip() + bearer_token = req.bearer_token.strip() or str(cfg.get("bearer_token", "") or "").strip() + + if not new_base_url: + raise HTTPException(status_code=400, detail="请填写平台地址") + + verified_token = bearer_token + if new_email and new_password: + verify = await run_in_threadpool(_verify_sub2api_login, new_base_url, new_email, new_password) + if not verify["ok"]: + raise HTTPException(status_code=400, detail=verify["error"]) + verified_token = str(verify.get("token") or "").strip() or bearer_token + elif bearer_token: + verify = await run_in_threadpool(_verify_sub2api_token, new_base_url, bearer_token) + if not verify["ok"]: + raise HTTPException(status_code=400, detail=verify["error"]) + else: + raise HTTPException(status_code=400, detail="请填写 Bearer Token 或邮箱和密码") + + upload_mode = str(req.upload_mode or "snapshot").strip().lower() + if upload_mode not in ("snapshot", "decoupled"): + upload_mode = "snapshot" + + cfg.update({ + "base_url": new_base_url, + "bearer_token": verified_token, + "email": new_email, + "password": new_password, + "account_name": req.account_name.strip(), + "auto_sync": req.auto_sync, + "upload_mode": upload_mode, + "sub2api_min_candidates": max(1, req.sub2api_min_candidates), + "sub2api_auto_maintain": req.sub2api_auto_maintain, + "sub2api_maintain_interval_minutes": max(5, req.sub2api_maintain_interval_minutes), + "sub2api_maintain_actions": _normalize_sub2api_maintain_actions(req.sub2api_maintain_actions), + "multithread": req.multithread, + "thread_count": max(1, min(req.thread_count, 10)), + "auto_register": req.auto_register, + }) + # 清理历史遗留字段 + cfg.pop("headful", None) + _save_sync_config(cfg) + _clear_sub2api_accounts_cache() + + # 先停再启,确保旧线程已退出 + _stop_sub2api_auto_maintain() + if req.sub2api_auto_maintain: + _start_sub2api_auto_maintain() + + return {"status": "saved", "verified": True} + + +@app.post("/api/sync-now") +async def api_sync_now(req: SyncNowRequest) -> Dict[str, Any]: + """手动触发同步:将本地 Token 文件完整导入 Sub2Api 平台""" + def _sync_now() -> Dict[str, Any]: + cfg = _get_sync_config() + base_url = str(cfg.get("base_url", "") or "").strip() + bearer = str(cfg.get("bearer_token", "") or "").strip() + if not base_url or not bearer: + raise HTTPException(status_code=400, detail="请先配置 Sub2Api 平台地址和 Bearer Token") + + results = [] + fnames = list(req.filenames or []) + if not fnames and os.path.isdir(TOKENS_DIR): + fnames = [f for f in os.listdir(TOKENS_DIR) if f.endswith(".json")] + + for fname in fnames: + if "/" in fname or "\\" in fname or ".." in fname: + continue + fpath = os.path.join(TOKENS_DIR, fname) + if not os.path.isfile(fpath): + results.append({"file": fname, "ok": False, "error": "文件不存在"}) + continue + try: + with open(fpath, "r", encoding="utf-8") as f: + data = json.load(f) + email = data.get("email", fname) + result = _push_account_api_with_dedupe( + base_url=base_url, + bearer=bearer, + email=str(email), + token_data=data, + check_before=True, + check_after=True, + ) + if result["ok"]: + _mark_token_uploaded_platform(fpath, "sub2api") + results.append({ + "file": fname, + "email": email, + "ok": result["ok"], + "status": result["status"], + "body": str(result["body"] or "")[:200], + }) + except Exception as e: + results.append({"file": fname, "ok": False, "error": str(e)}) + + ok_count = sum(1 for r in results if r["ok"]) + fail_count = len(results) - ok_count + return {"total": len(results), "ok": ok_count, "fail": fail_count, "results": results} + + return await run_in_threadpool(_sync_now) + + +class Sub2ApiLoginRequest(BaseModel): + base_url: str + email: str + password: str + + +@app.post("/api/sub2api-login") +async def api_sub2api_login(req: Sub2ApiLoginRequest) -> Dict[str, Any]: + """用账号密码登录 Sub2Api 平台,自动获取并保存 Bearer Token""" + def _login() -> Dict[str, Any]: + cfg = _get_sync_config() + base_url = req.base_url.strip() + if not base_url: + raise HTTPException(status_code=400, detail="请填写平台地址") + if not base_url.startswith(("http://", "https://")): + base_url = "https://" + base_url + + login_url = base_url.rstrip("/") + "/api/v1/auth/login" + payload = json.dumps({"email": req.email, "password": req.password}).encode("utf-8") + request = urllib.request.Request( + login_url, + data=payload, + method="POST", + headers={"Content-Type": "application/json", "Accept": "application/json"}, + ) + try: + with urllib.request.urlopen(request, timeout=15) as resp: + raw_body = resp.read().decode("utf-8") + try: + body = json.loads(raw_body) + except json.JSONDecodeError: + raise HTTPException(status_code=502, detail=f"服务器返回非 JSON 格式: {raw_body[:200]}") + except urllib.error.HTTPError as exc: + raw = exc.read().decode("utf-8", "replace") + try: + err_body = json.loads(raw) + err_msg = err_body.get("message") or err_body.get("error") or raw[:200] + except json.JSONDecodeError: + err_msg = raw[:200] + raise HTTPException(status_code=exc.code, detail=f"登录失败: {err_msg}") + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=f"请求异常: {e}") + + token = ( + body.get("token") + or body.get("access_token") + or (body.get("data") or {}).get("token") + or (body.get("data") or {}).get("access_token") + or "" + ) + if not token: + raise HTTPException(status_code=502, detail=f"响应中未找到 token 字段: {str(body)[:300]}") + + cfg["base_url"] = base_url + cfg["bearer_token"] = token + _save_sync_config(cfg) + return {"ok": True, "token_preview": token[:16] + "..."} + + return await run_in_threadpool(_login) + + +@app.post("/api/check-proxy") +async def api_check_proxy(req: ProxyCheckRequest) -> Dict[str, Any]: + """检测代理是否可用(通过 Cloudflare Trace)""" + def _check() -> Dict[str, Any]: + proxy = req.proxy.strip() + try: + from curl_cffi import requests as cffi_req + import re + + proxies = {"http": proxy, "https": proxy} if proxy else None + try: + resp = cffi_req.get( + "https://cloudflare.com/cdn-cgi/trace", + proxies=proxies, + http_version="v2", + impersonate="chrome", + timeout=8, + ) + except Exception as exc: + if "HTTP/3 is not supported over an HTTP proxy" not in str(exc): + raise + resp = cffi_req.get( + "https://cloudflare.com/cdn-cgi/trace", + proxies=proxies, + http_version="v1", + impersonate="chrome", + timeout=8, + ) + text = resp.text + loc_m = re.search(r"^loc=(.+)$", text, re.MULTILINE) + loc = loc_m.group(1) if loc_m else "?" + supported = loc not in ("CN", "HK") + return {"ok": supported, "loc": loc, "error": None if supported else "所在地不支持"} + except Exception as e: + return {"ok": False, "loc": None, "error": str(e)} + + return await run_in_threadpool(_check) + + +@app.post("/api/proxy-pool/test") +async def api_proxy_pool_test(req: ProxyPoolTestRequest) -> Dict[str, Any]: + """测试代理池取号:返回取到的代理与可选 loc 探测结果""" + def _test() -> Dict[str, Any]: + cfg_snapshot = _get_sync_config() + auth_mode = str(req.auth_mode or "query").strip().lower() + if auth_mode not in ("header", "query"): + auth_mode = "query" + api_url = str(req.api_url or "https://zenproxy.top/api/fetch").strip() or "https://zenproxy.top/api/fetch" + api_key = req.api_key.strip() if req.api_key else str(cfg_snapshot.get("proxy_pool_api_key", "")).strip() + try: + count = max(1, min(int(req.count or cfg_snapshot.get("proxy_pool_count", 1)), 20)) + except (TypeError, ValueError): + count = 1 + country = str(req.country or cfg_snapshot.get("proxy_pool_country", "US") or "US").strip().upper() or "US" + + cfg = { + "enabled": bool(req.enabled), + "api_url": api_url, + "auth_mode": auth_mode, + "api_key": api_key, + "count": count, + "country": country, + "timeout_seconds": 10, + } + if not cfg["enabled"]: + return {"ok": False, "error": "代理池未启用"} + if not cfg["api_key"]: + return {"ok": False, "error": "API Key 为空"} + + try: + from curl_cffi import requests as cffi_req + import re + + relay_url = _pool_relay_url_from_fetch_url(api_url) + if relay_url: + relay_params = {"api_key": api_key, "url": "https://cloudflare.com/cdn-cgi/trace", "country": country} + try: + relay_resp = cffi_req.get(relay_url, params=relay_params, http_version="v2", impersonate="chrome", timeout=8) + except Exception as exc: + if "HTTP/3 is not supported over an HTTP proxy" not in str(exc): + raise + relay_resp = cffi_req.get(relay_url, params=relay_params, http_version="v1", impersonate="chrome", timeout=8) + if relay_resp.status_code == 200: + relay_text = relay_resp.text + relay_loc_m = re.search(r"^loc=(.+)$", relay_text, re.MULTILINE) + relay_loc = relay_loc_m.group(1) if relay_loc_m else "?" + relay_supported = relay_loc not in ("CN", "HK") + return { + "ok": True, + "proxy": "(relay)", + "relay_used": True, + "relay_url": relay_url, + "count": count, + "country": country, + "loc": relay_loc, + "supported": relay_supported, + "trace_error": None, + } + + proxy = _fetch_proxy_from_pool(cfg) + proxies = {"http": proxy, "https": proxy} if proxy else None + trace_error = "" + loc = None + supported = None + try: + try: + resp = cffi_req.get("https://cloudflare.com/cdn-cgi/trace", proxies=proxies, http_version="v2", impersonate="chrome", timeout=8) + except Exception as exc: + if "HTTP/3 is not supported over an HTTP proxy" not in str(exc): + raise + resp = cffi_req.get("https://cloudflare.com/cdn-cgi/trace", proxies=proxies, http_version="v1", impersonate="chrome", timeout=8) + text = resp.text + loc_m = re.search(r"^loc=(.+)$", text, re.MULTILINE) + loc = loc_m.group(1) if loc_m else "?" + supported = loc not in ("CN", "HK") + except Exception as e: + trace_error = str(e) + + return { + "ok": True, + "proxy": proxy, + "relay_used": False, + "count": count, + "country": country, + "loc": loc, + "supported": supported, + "trace_error": trace_error or None, + } + except Exception as e: + return {"ok": False, "error": str(e)} + + return await run_in_threadpool(_test) + + +@app.get("/api/logs") +async def api_logs(request: Request) -> StreamingResponse: + """SSE 实时结构化事件流""" + + async def event_generator() -> AsyncGenerator[str, None]: + q = _state.subscribe() + last_heartbeat = time.monotonic() + try: + snapshot = _state.get_status_snapshot() + connected = { + "type": "connected", + "message": "日志连接成功", + "run_id": snapshot["task"].get("run_id"), + "revision": snapshot["task"].get("revision", 0), + "snapshot": snapshot, + } + yield f"event: connected\ndata: {json.dumps(connected, ensure_ascii=False)}\n\n" + while True: + if _service_shutdown_event.is_set(): + break + if await request.is_disconnected(): + break + try: + event = await asyncio.wait_for(q.get(), timeout=1.0) + event_type = str(event.get("type") or "message") + yield f"event: {event_type}\ndata: {json.dumps(event, ensure_ascii=False)}\n\n" + if _service_shutdown_event.is_set() or str(event.get("step") or "").strip().lower() == "shutdown": + break + except asyncio.TimeoutError: + if _service_shutdown_event.is_set(): + break + now = time.monotonic() + if now - last_heartbeat >= 15: + last_heartbeat = now + heartbeat = { + "type": "heartbeat", + "run_id": _state.run_id, + "revision": _state.revision, + "server_time": datetime.now().isoformat(timespec="seconds"), + } + yield f"event: heartbeat\ndata: {json.dumps(heartbeat, ensure_ascii=False)}\n\n" + except asyncio.CancelledError: + break + except Exception: + break + finally: + _state.unsubscribe(q) + + return StreamingResponse( + event_generator(), + media_type="text/event-stream", + headers={ + "Cache-Control": "no-cache", + "X-Accel-Buffering": "no", + }, + ) + + + +class BatchSyncRequest(BaseModel): + filenames: List[str] = [] # 空列表 = 同步全部 + + +def _decode_jwt_payload(token: str) -> Dict[str, Any]: + """解析 JWT payload(不验签)""" + try: + parts = token.split(".") + if len(parts) != 3: + return {} + payload = parts[1] + pad = 4 - len(payload) % 4 + if pad != 4: + payload += "=" * pad + import base64 as _b64 + decoded = _b64.urlsafe_b64decode(payload.encode("ascii")) + return json.loads(decoded.decode("utf-8")) + except Exception: + return {} + + +def _build_account_payload(email: str, token_data: Dict[str, Any]) -> Dict[str, Any]: + """参考 chatgpt_register.py 构建 /api/v1/admin/accounts 所需 payload""" + access_token = token_data.get("access_token", "") + refresh_token = token_data.get("refresh_token", "") + id_token = token_data.get("id_token", "") + + at_payload = _decode_jwt_payload(access_token) if access_token else {} + at_auth = at_payload.get("https://api.openai.com/auth") or {} + chatgpt_account_id = at_auth.get("chatgpt_account_id", "") or token_data.get("account_id", "") + chatgpt_user_id = at_auth.get("chatgpt_user_id", "") + exp_timestamp = at_payload.get("exp", 0) + expires_at = exp_timestamp if isinstance(exp_timestamp, int) and exp_timestamp > 0 else int(time.time()) + 863999 + + it_payload = _decode_jwt_payload(id_token) if id_token else {} + it_auth = it_payload.get("https://api.openai.com/auth") or {} + organization_id = it_auth.get("organization_id", "") + if not organization_id: + orgs = it_auth.get("organizations") or [] + if orgs: + organization_id = (orgs[0] or {}).get("id", "") + + return { + "name": email, + "notes": "", + "platform": "openai", + "type": "oauth", + "credentials": { + "access_token": access_token, + "refresh_token": refresh_token, + "expires_in": 863999, + "expires_at": expires_at, + "chatgpt_account_id": chatgpt_account_id, + "chatgpt_user_id": chatgpt_user_id, + "organization_id": organization_id, + }, + "extra": {"email": email}, + "proxy_id": None, + "concurrency": 10, + "priority": 1, + "rate_multiplier": 1, + "group_ids": [2, 4], + "expires_at": None, + "auto_pause_on_expired": True, + } + + +def _push_account_api(base_url: str, bearer: str, email: str, token_data: Dict[str, Any]) -> Dict[str, Any]: + """调用 /api/v1/admin/accounts 提交完整账号信息""" + from curl_cffi import requests as cffi_req + url = base_url.rstrip("/") + "/api/v1/admin/accounts" + payload = _build_account_payload(email, token_data) + try: + resp = cffi_req.post( + url, + json=payload, + headers={ + "Authorization": f"Bearer {bearer}", + "Content-Type": "application/json", + "Accept": "application/json, text/plain, */*", + "Referer": base_url.rstrip("/") + "/admin/accounts", + }, + impersonate="chrome", + timeout=20, + ) + return {"ok": resp.status_code in (200, 201), "status": resp.status_code, "body": resp.text[:300]} + except Exception as e: + return {"ok": False, "status": 0, "body": str(e)} + + +def _update_sub2api_account_api( + base_url: str, + bearer: str, + account_id: int, + email: str, + token_data: Dict[str, Any], +) -> Dict[str, Any]: + """ + 命中已存在账号后,更新其凭据,避免“存在即跳过”导致账号长期不刷新。 + """ + from curl_cffi import requests as cffi_req + + url = base_url.rstrip("/") + f"/api/v1/admin/accounts/{int(account_id)}" + create_payload = _build_account_payload(email, token_data) + credentials = create_payload.get("credentials") if isinstance(create_payload.get("credentials"), dict) else {} + extra = create_payload.get("extra") if isinstance(create_payload.get("extra"), dict) else {} + payload = { + "name": str(email or "").strip(), + "credentials": credentials, + "extra": extra, + "concurrency": create_payload.get("concurrency", 10), + "priority": create_payload.get("priority", 1), + "status": "active", + "auto_pause_on_expired": True, + } + try: + resp = cffi_req.put( + url, + json=payload, + headers={ + "Authorization": f"Bearer {bearer}", + "Content-Type": "application/json", + "Accept": "application/json, text/plain, */*", + "Referer": base_url.rstrip("/") + "/admin/accounts", + }, + impersonate="chrome", + timeout=20, + ) + return {"ok": resp.status_code in (200, 201), "status": resp.status_code, "body": resp.text[:300]} + except Exception as e: + return {"ok": False, "status": 0, "body": str(e)} + + +def _extract_sub2api_page_payload(body: Any) -> Dict[str, Any]: + if isinstance(body, dict): + data = body.get("data") + if isinstance(data, dict): + return data + return body + return {} + + +def _sub2api_identity_keys(email: str, refresh_token: str) -> List[str]: + keys: List[str] = [] + email_norm = str(email or "").strip().lower() + refresh_token_norm = str(refresh_token or "").strip() + if email_norm: + keys.append(f"email:{email_norm}") + if refresh_token_norm: + keys.append(f"rt:{refresh_token_norm}") + return keys + + +def _load_local_token_identity_keys(max_files: int = 20000) -> set[str]: + """ + 预加载本地 token 文件身份键,用于运行前去重(跨线程/跨重启防重复落盘)。 + """ + keys: set[str] = set() + if not os.path.isdir(TOKENS_DIR): + return keys + + loaded = 0 + for fname in os.listdir(TOKENS_DIR): + if loaded >= max_files: + break + if not str(fname).endswith(".json"): + continue + fpath = os.path.join(TOKENS_DIR, fname) + if not os.path.isfile(fpath): + continue + try: + with open(fpath, "r", encoding="utf-8") as f: + td = json.load(f) + if not isinstance(td, dict): + continue + email = str(td.get("email") or "").strip() + refresh_token = str(td.get("refresh_token") or "").strip() + keys.update(_sub2api_identity_keys(email, refresh_token)) + loaded += 1 + except Exception: + continue + return keys + + +def _sub2api_item_matches_identity(item: Dict[str, Any], email: str, refresh_token: str) -> bool: + email_norm = str(email or "").strip().lower() + refresh_token_norm = str(refresh_token or "").strip() + + name = str(item.get("name") or "").strip().lower() + extra = item.get("extra") if isinstance(item.get("extra"), dict) else {} + credentials = item.get("credentials") if isinstance(item.get("credentials"), dict) else {} + item_email = str(extra.get("email") or "").strip().lower() + item_refresh_token = str(credentials.get("refresh_token") or "").strip() + + if refresh_token_norm and item_refresh_token and item_refresh_token == refresh_token_norm: + return True + if email_norm and (name == email_norm or item_email == email_norm): + return True + return False + + +def _find_existing_sub2api_account( + base_url: str, + bearer: str, + email: str, + refresh_token: str, + max_pages: int = 8, +) -> Optional[Dict[str, Any]]: + """ + 在 Sub2Api 端查找是否已存在同一身份账号(email / refresh_token)。 + 说明: + - 主查 email(search 参数),并在返回项里再次精确匹配; + - 若首次未命中,且提供了 refresh_token,会在有限页内继续扫一遍做 token 精确匹配。 + """ + from curl_cffi import requests as cffi_req + + url = base_url.rstrip("/") + "/api/v1/admin/accounts" + email_norm = str(email or "").strip().lower() + refresh_token_norm = str(refresh_token or "").strip() + if not email_norm and not refresh_token_norm: + return None + + headers = { + "Authorization": f"Bearer {bearer}", + "Accept": "application/json, text/plain, */*", + } + + page_size = 100 + page = 1 + scanned_without_search = 0 + + while page <= max_pages: + params: Dict[str, Any] = { + "page": page, + "page_size": page_size, + "platform": "openai", + "type": "oauth", + } + if email_norm: + params["search"] = email_norm + + try: + resp = cffi_req.get( + url, + params=params, + headers=headers, + impersonate="chrome", + timeout=15, + ) + if resp.status_code != 200: + return None + body = resp.json() + except Exception: + return None + + data = _extract_sub2api_page_payload(body) + items = data.get("items") if isinstance(data.get("items"), list) else [] + for item in items: + if isinstance(item, dict) and _sub2api_item_matches_identity(item, email_norm, refresh_token_norm): + return item + + total_raw = data.get("total") + try: + total = int(total_raw) if total_raw is not None else 0 + except (TypeError, ValueError): + total = 0 + if len(items) < page_size or (total > 0 and page * page_size >= total): + break + page += 1 + + # search=xxx 未命中时,额外做有限页扫描,用 refresh_token 做兜底精确匹配 + if refresh_token_norm: + page = 1 + while page <= 3: + params = { + "page": page, + "page_size": page_size, + "platform": "openai", + "type": "oauth", + } + try: + resp = cffi_req.get( + url, + params=params, + headers=headers, + impersonate="chrome", + timeout=15, + ) + if resp.status_code != 200: + return None + body = resp.json() + except Exception: + return None + + data = _extract_sub2api_page_payload(body) + items = data.get("items") if isinstance(data.get("items"), list) else [] + for item in items: + if isinstance(item, dict) and _sub2api_item_matches_identity(item, "", refresh_token_norm): + return item + + scanned_without_search += len(items) + if len(items) < page_size or scanned_without_search >= 300: + break + page += 1 + + return None + + +def _push_account_api_with_dedupe( + base_url: str, + bearer: str, + email: str, + token_data: Dict[str, Any], + check_before: bool = True, + check_after: bool = True, +) -> Dict[str, Any]: + """ + 上传前后做远端查重,避免重复创建同一账号。 + 返回结构兼容 _push_account_api,额外包含: + - skipped: bool + - reason: str + - existing_id: Optional[int] + """ + refresh_token = str(token_data.get("refresh_token") or "").strip() + existing: Optional[Dict[str, Any]] = None + + if check_before: + existing = _find_existing_sub2api_account(base_url, bearer, email, refresh_token) + if existing is not None: + existing_id = existing.get("id") + existing_int = None + try: + existing_int = int(existing_id) + except (TypeError, ValueError): + existing_int = None + if existing_int is not None and existing_int > 0: + update_result = _update_sub2api_account_api( + base_url=base_url, + bearer=bearer, + account_id=existing_int, + email=email, + token_data=token_data, + ) + if update_result.get("ok"): + return { + "ok": True, + "status": int(update_result.get("status") or 200), + "body": "existing account updated", + "skipped": False, + "reason": "updated_existing_before_create", + "existing_id": existing_int, + } + return { + "ok": False, + "status": int(update_result.get("status") or 0), + "body": "existing account update failed", + "skipped": False, + "reason": "exists_before_create_update_failed", + "existing_id": existing_int, + "update_status": int(update_result.get("status") or 0), + "update_body": str(update_result.get("body") or "")[:240], + } + return { + "ok": True, + "status": 200, + "body": "account already exists", + "skipped": True, + "reason": "exists_before_create", + "existing_id": existing_id, + } + + result = _push_account_api(base_url, bearer, email, token_data) + if result.get("ok"): + result["skipped"] = False + return result + + if check_after: + existing = _find_existing_sub2api_account(base_url, bearer, email, refresh_token) + if existing is not None: + return { + "ok": True, + "status": int(result.get("status") or 200), + "body": "request failed but account exists", + "skipped": True, + "reason": "exists_after_create", + "existing_id": existing.get("id"), + } + + result.setdefault("skipped", False) + return result + + +@app.post("/api/sync-batch") +async def api_sync_batch(req: BatchSyncRequest) -> Dict[str, Any]: + """通过 HTTP API 将本地 Token 批量导入 Sub2Api 平台""" + def _sync_batch() -> Dict[str, Any]: + cfg = _get_sync_config() + base_url = str(cfg.get("base_url", "") or "").strip() + bearer = str(cfg.get("bearer_token", "") or "").strip() + + if not base_url: + raise HTTPException(status_code=400, detail="请先配置 Sub2Api 平台地址") + if not bearer: + raise HTTPException(status_code=400, detail="Bearer Token 为空,请重新保存配置以自动登录获取") + + fnames = list(req.filenames or []) + if not fnames: + fnames = [f for f in os.listdir(TOKENS_DIR) if f.endswith(".json")] + + results = [] + for fname in fnames: + if "/" in fname or "\\" in fname or ".." in fname: + continue + fpath = os.path.join(TOKENS_DIR, fname) + if not os.path.isfile(fpath): + results.append({"file": fname, "ok": False, "error": "文件不存在"}) + continue + try: + with open(fpath, "r", encoding="utf-8") as f: + token_data = json.load(f) + email = token_data.get("email", fname) + if _is_sub2api_uploaded(token_data): + results.append({"file": fname, "email": email, "ok": True, "skipped": True}) + continue + result = _push_account_api_with_dedupe( + base_url=base_url, + bearer=bearer, + email=str(email), + token_data=token_data, + check_before=True, + check_after=True, + ) + results.append({"file": fname, "email": email, **result}) + if result["ok"]: + _mark_token_uploaded_platform(fpath, "sub2api") + reason = str(result.get("reason") or "") + if reason == "updated_existing_before_create": + _state.broadcast({ + "ts": datetime.now().strftime("%H:%M:%S"), + "level": "success", + "message": f"[API] {email}: 命中已存在账号并更新凭据 (id={result.get('existing_id', '-')})", + "step": "sync", + }) + elif result.get("skipped"): + _state.broadcast({ + "ts": datetime.now().strftime("%H:%M:%S"), + "level": "success", + "message": f"[API] {email}: 同步成功", + "step": "sync", + }) + else: + _state.broadcast({ + "ts": datetime.now().strftime("%H:%M:%S"), + "level": "success", + "message": f"[API] {email}: 导入成功", + "step": "sync", + }) + else: + _state.broadcast({ + "ts": datetime.now().strftime("%H:%M:%S"), + "level": "error", + "message": f"[API] {email}: 导入失败({result['status']}) {result['body'][:100]}", + "step": "sync", + }) + except Exception as e: + results.append({"file": fname, "ok": False, "error": str(e)}) + + ok_count = sum(1 for r in results if r.get("ok") and not r.get("skipped")) + skip_count = sum(1 for r in results if r.get("skipped")) + fail_count = sum(1 for r in results if not r.get("ok")) + return {"total": len(results), "ok": ok_count, "skipped": skip_count, "fail": fail_count, "results": results} + + return await run_in_threadpool(_sync_batch) + + +# ========================================== +# Pool / Mail 配置 & 维护 API +# ========================================== + + +class PoolConfigRequest(BaseModel): + cpa_base_url: str = "" + cpa_token: str = "" + min_candidates: int = 800 + used_percent_threshold: int = 95 + auto_maintain: bool = False + maintain_interval_minutes: int = 30 + + +class MailConfigRequest(BaseModel): + mail_provider: str = "mailtm" + mail_config: Dict[str, str] = {} + mail_providers: List[str] = [] + mail_provider_configs: Dict[str, Dict[str, str]] = {} + mail_strategy: str = "round_robin" + + +@app.get("/api/pool/config") +async def api_get_pool_config() -> Dict[str, Any]: + cfg = _get_sync_config() + token = str(cfg.get("cpa_token", "")) + return { + "cpa_base_url": cfg.get("cpa_base_url", ""), + "cpa_token_preview": (token[:12] + "...") if len(token) > 12 else token, + "min_candidates": cfg.get("min_candidates", 800), + "used_percent_threshold": cfg.get("used_percent_threshold", 95), + "auto_maintain": cfg.get("auto_maintain", False), + "maintain_interval_minutes": cfg.get("maintain_interval_minutes", 30), + } + + +@app.post("/api/pool/config") +async def api_set_pool_config(req: PoolConfigRequest) -> Dict[str, Any]: + cfg = _get_sync_config() + cfg["cpa_base_url"] = req.cpa_base_url.strip() + cfg["cpa_token"] = req.cpa_token.strip() or str(cfg.get("cpa_token", "") or "").strip() + cfg["min_candidates"] = req.min_candidates + cfg["used_percent_threshold"] = req.used_percent_threshold + cfg["auto_maintain"] = req.auto_maintain + cfg["maintain_interval_minutes"] = max(5, req.maintain_interval_minutes) + _save_sync_config(cfg) + + # 启停自动维护 + if req.auto_maintain: + _start_auto_maintain() + else: + _stop_auto_maintain() + + return {"status": "saved"} + + +@app.get("/api/pool/status") +async def api_pool_status() -> Dict[str, Any]: + pm = _get_pool_maintainer() + if not pm: + return {"configured": False, "error": "CPA 未配置"} + status = await run_in_threadpool(pm.get_pool_status) + status["configured"] = True + return status + + +@app.post("/api/pool/check") +async def api_pool_check() -> Dict[str, Any]: + pm = _get_pool_maintainer() + if not pm: + raise HTTPException(status_code=400, detail="CPA 未配置") + result = await run_in_threadpool(pm.test_connection) + return result + + +@app.post("/api/pool/maintain") +async def api_pool_maintain() -> Dict[str, Any]: + pm = _get_pool_maintainer() + if not pm: + raise HTTPException(status_code=400, detail="CPA 未配置") + if not _pool_maintain_lock.acquire(blocking=False): + raise HTTPException(status_code=409, detail="维护任务已在执行中") + try: + result = await run_in_threadpool(pm.probe_and_clean_sync) + _state.broadcast({ + "ts": datetime.now().strftime("%H:%M:%S"), + "level": "info", + "message": f"[POOL] 维护完成: 无效 {result.get('invalid_count', 0)}, 已删除 {result.get('deleted_ok', 0)}", + "step": "pool_maintain", + }) + return result + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + finally: + _pool_maintain_lock.release() + + +@app.post("/api/pool/auto") +async def api_pool_auto(enable: bool = True) -> Dict[str, Any]: + cfg = _get_sync_config() + cfg["auto_maintain"] = enable + _save_sync_config(cfg) + if enable: + _start_auto_maintain() + else: + _stop_auto_maintain() + return {"auto_maintain": enable} + + +@app.get("/api/mail/config") +async def api_get_mail_config() -> Dict[str, Any]: + cfg = _get_sync_config() + # 兼容旧格式 + mail_cfg = dict(cfg.get("mail_config") or {}) + token = str(mail_cfg.get("bearer_token", "")) + mail_cfg["bearer_token_preview"] = (token[:12] + "...") if len(token) > 12 else token + mail_cfg.pop("bearer_token", None) + key = str(mail_cfg.get("api_key", "")) + mail_cfg["api_key_preview"] = (key[:8] + "...") if len(key) > 8 else key + mail_cfg.pop("api_key", None) + + # 脱敏 provider_configs 中的敏感字段 + raw_configs = cfg.get("mail_provider_configs") or {} + safe_configs: Dict[str, Dict] = {} + for pname, pcfg in raw_configs.items(): + sc = dict(pcfg) + for secret_key in ("bearer_token", "api_key", "admin_password"): + val = str(sc.get(secret_key, "")) + if val: + sc[f"{secret_key}_preview"] = (val[:8] + "...") if len(val) > 8 else val + sc.pop(secret_key, None) + safe_configs[pname] = sc + + return { + "mail_provider": cfg.get("mail_provider", "mailtm"), + "mail_config": mail_cfg, + "mail_providers": cfg.get("mail_providers", []), + "mail_provider_configs": safe_configs, + "mail_strategy": cfg.get("mail_strategy", "round_robin"), + } + + +@app.post("/api/mail/config") +async def api_set_mail_config(req: MailConfigRequest) -> Dict[str, Any]: + cfg = _get_sync_config() + # 兼容旧格式 + cfg["mail_provider"] = req.mail_provider.strip() or "mailtm" + cfg["mail_config"] = {str(k): str(v).strip() for k, v in (req.mail_config or {}).items()} + + # 新多提供商格式 + if req.mail_providers: + cfg["mail_providers"] = [str(name).strip().lower() for name in req.mail_providers if str(name).strip()] + cfg["mail_strategy"] = req.mail_strategy or "round_robin" + + existing_configs = cfg.get("mail_provider_configs") or {} + for pname, pcfg in req.mail_provider_configs.items(): + existing_configs[str(pname).strip().lower()] = { + str(k): str(v).strip() for k, v in (pcfg or {}).items() + } + cfg["mail_provider_configs"] = existing_configs + + _save_sync_config(cfg) + return {"status": "saved"} + + +@app.post("/api/mail/test") +async def api_mail_test() -> Dict[str, Any]: + try: + cfg = _get_sync_config() + router = MultiMailRouter(cfg) + results = [] + proxy = str(cfg.get("proxy") or _state.current_proxy or "").strip() + for pname, provider in router.providers(): + ok, msg = await run_in_threadpool(provider.test_connection, proxy) + results.append({"provider": pname, "ok": ok, "message": msg}) + all_ok = all(r["ok"] for r in results) + return {"ok": all_ok, "results": results, "message": "全部通过" if all_ok else "部分失败"} + except Exception as e: + return {"ok": False, "message": str(e)} + + +def _try_auto_register() -> None: + """维护后检查池状态,若不足则自动启动注册补充""" + ts = datetime.now().strftime("%H:%M:%S") + cfg = _get_sync_config() + if not cfg.get("auto_register"): + _state.broadcast({ + "ts": ts, "level": "info", + "message": "[AUTO] 自动注册未开启,跳过(请勾选「池不足自动注册」并保存代理)", + "step": "auto_register", + }) + return + proxy = str(cfg.get("proxy", "") or "").strip() + proxy_pool_enabled = bool(cfg.get("proxy_pool_enabled", False)) + if not proxy and not proxy_pool_enabled: + _state.broadcast({ + "ts": ts, "level": "warn", + "message": "[AUTO] 跳过自动注册:未配置固定代理且代理池未启用,请先配置", + "step": "auto_register", + }) + return + if _state.status != "idle": + _state.broadcast({ + "ts": ts, "level": "info", + "message": f"[AUTO] 跳过自动注册:当前状态 {_state.status}", + "step": "auto_register", + }) + return + upload_mode = str(cfg.get("upload_mode", "snapshot") or "snapshot").strip().lower() + if upload_mode not in ("snapshot", "decoupled"): + upload_mode = "snapshot" + gap = 0 + cpa_gap = 0 + sub2api_gap = 0 + api_error = False + pm = _get_pool_maintainer(cfg) + if pm: + try: + cpa_gap = pm.calculate_gap() + except Exception as e: + api_error = True + _state.broadcast({ + "ts": ts, "level": "warn", + "message": f"[AUTO] CPA 池状态查询失败,稍后重试: {e}", + "step": "auto_register", + }) + sm = _get_sub2api_maintainer(cfg) + if sm and _is_auto_sync_enabled(cfg): + try: + sub2api_gap = sm.calculate_gap() + except Exception as e: + api_error = True + _state.broadcast({ + "ts": ts, "level": "warn", + "message": f"[AUTO] Sub2Api 池状态查询失败,稍后重试: {e}", + "step": "auto_register", + }) + elif sm: + _state.broadcast({ + "ts": ts, "level": "info", + "message": "[AUTO] Sub2Api 自动同步未开启,自动补号仅按 CPA 缺口执行", + "step": "auto_register", + }) + gap = (cpa_gap + sub2api_gap) if upload_mode == "snapshot" else max(cpa_gap, sub2api_gap) + if api_error and gap <= 0: + return + if gap <= 0: + _state.broadcast({ + "ts": ts, "level": "info", + "message": "[AUTO] 池已充足,无需补充注册", + "step": "auto_register", + }) + return + multithread = bool(cfg.get("multithread", False)) + thread_count = int(cfg.get("thread_count", 3)) + try: + _state.start_task( + proxy, + worker_count=thread_count if multithread else 1, + target_count=gap, + cpa_target_count=cpa_gap if pm else 0, + sub2api_target_count=sub2api_gap if sm and _is_auto_sync_enabled(cfg) else 0, + ) + _state.broadcast({ + "ts": ts, "level": "success", + "message": ( + f"[AUTO] 自动注册已启动:总补充 {gap}(CPA 缺口 {cpa_gap} / Sub2Api 缺口 {sub2api_gap} / " + f"策略 {upload_mode})" + ), + "step": "auto_register", + }) + except RuntimeError as e: + _state.broadcast({ + "ts": ts, "level": "warn", + "message": f"[AUTO] 自动注册启动失败:{e}", + "step": "auto_register", + }) + + +def _start_auto_maintain() -> None: + global _auto_maintain_thread, _auto_maintain_stop + cfg = _get_sync_config() + interval = max(5, int(cfg.get("maintain_interval_minutes", 30))) * 60 + with _auto_maintain_ctl_lock: + if _auto_maintain_thread and _auto_maintain_thread.is_alive(): + return + stop_event = threading.Event() + _auto_maintain_stop = stop_event + + def _loop(local_stop: threading.Event) -> None: + while not local_stop.is_set(): + pm = _get_pool_maintainer() + if pm: + if not _pool_maintain_lock.acquire(blocking=False): + _state.broadcast({ + "ts": datetime.now().strftime("%H:%M:%S"), + "level": "warn", + "message": "[POOL] 跳过自动维护:已有维护任务在执行", + "step": "pool_auto", + }) + else: + try: + result = pm.probe_and_clean_sync() + _state.broadcast({ + "ts": datetime.now().strftime("%H:%M:%S"), + "level": "info", + "message": f"[POOL] 自动维护: 无效 {result.get('invalid_count', 0)}, 已删除 {result.get('deleted_ok', 0)}", + "step": "pool_auto", + }) + except Exception as e: + _state.broadcast({ + "ts": datetime.now().strftime("%H:%M:%S"), + "level": "error", + "message": f"[POOL] 自动维护异常: {e}", + "step": "pool_auto", + }) + finally: + _pool_maintain_lock.release() + _try_auto_register() + local_stop.wait(interval) + + thread = threading.Thread(target=_loop, args=(stop_event,), daemon=True) + with _auto_maintain_ctl_lock: + _auto_maintain_thread = thread + thread.start() + + +def _stop_auto_maintain() -> None: + global _auto_maintain_thread, _auto_maintain_stop + with _auto_maintain_ctl_lock: + stop_event = _auto_maintain_stop + thread = _auto_maintain_thread + if stop_event: + stop_event.set() + if thread and thread.is_alive(): + thread.join(timeout=5) + with _auto_maintain_ctl_lock: + if _auto_maintain_thread is thread and (thread is None or not thread.is_alive()): + _auto_maintain_thread = None + _auto_maintain_stop = None + + +# ========================================== +# Sub2Api 池维护 API & 自动维护 +# ========================================== + +_sub2api_auto_maintain_thread: Optional[threading.Thread] = None +_sub2api_auto_maintain_stop: Optional[threading.Event] = None +_sub2api_auto_maintain_ctl_lock = threading.Lock() +_sub2api_maintain_lock = threading.Lock() + + +class Sub2ApiDedupeRequest(BaseModel): + dry_run: bool = True + timeout: int = 20 + + +class Sub2ApiAccountActionRequest(BaseModel): + account_ids: List[int] = Field(default_factory=list) + timeout: int = 30 + + +class Sub2ApiExceptionHandleRequest(Sub2ApiAccountActionRequest): + delete_unresolved: bool = True + + +@app.get("/api/sub2api/accounts") +async def api_sub2api_accounts( + page: int = 1, + page_size: int = 20, + status: str = "all", + keyword: str = "", +) -> Dict[str, Any]: + sm = _get_sub2api_maintainer() + if not sm: + return {"configured": False, "error": "Sub2Api 未配置", "items": []} + cfg = _get_sync_config() + snapshot = await run_in_threadpool( + lambda: _get_sub2api_accounts_inventory_snapshot(sm, cfg) + ) + filtered_items = _filter_sub2api_account_items( + list(snapshot.get("items") or []), + status=status, + keyword=keyword, + ) + paged = _paginate_sub2api_account_items(filtered_items, page=page, page_size=page_size) + return { + "configured": True, + "total": int(snapshot.get("total", 0)), + "error_count": int(snapshot.get("error_count", 0)), + "duplicate_groups": int(snapshot.get("duplicate_groups", 0)), + "duplicate_accounts": int(snapshot.get("duplicate_accounts", 0)), + "items": paged["items"], + "page": paged["page"], + "page_size": paged["page_size"], + "filtered_total": paged["filtered_total"], + "total_pages": paged["total_pages"], + "status": str(status or "all"), + "keyword": str(keyword or ""), + } + + +@app.post("/api/sub2api/accounts/probe") +async def api_sub2api_accounts_probe(req: Sub2ApiAccountActionRequest) -> Dict[str, Any]: + sm = _get_sub2api_maintainer() + if not sm: + raise HTTPException(status_code=400, detail="Sub2Api 未配置") + if not req.account_ids: + raise HTTPException(status_code=400, detail="请先选择至少一个账号") + if not _sub2api_maintain_lock.acquire(blocking=False): + raise HTTPException(status_code=409, detail="Sub2Api 账号任务已在执行中") + try: + timeout = max(5, int(req.timeout)) + result = await run_in_threadpool( + lambda: sm.probe_accounts(req.account_ids, timeout=timeout) + ) + _state.broadcast({ + "ts": datetime.now().strftime("%H:%M:%S"), + "level": "info", + "message": ( + f"[Sub2Api] 账号测活: 请求 {result.get('requested', 0)}, " + f"刷新成功 {result.get('refreshed_ok', 0)}, " + f"恢复 {result.get('recovered', 0)}, " + f"仍异常 {result.get('still_abnormal', 0)}" + ), + "step": "sub2api_accounts_probe", + }) + _clear_sub2api_accounts_cache() + return result + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + finally: + _sub2api_maintain_lock.release() + + +@app.post("/api/sub2api/accounts/delete") +async def api_sub2api_accounts_delete(req: Sub2ApiAccountActionRequest) -> Dict[str, Any]: + sm = _get_sub2api_maintainer() + if not sm: + raise HTTPException(status_code=400, detail="Sub2Api 未配置") + if not req.account_ids: + raise HTTPException(status_code=400, detail="请先选择至少一个账号") + if not _sub2api_maintain_lock.acquire(blocking=False): + raise HTTPException(status_code=409, detail="Sub2Api 账号任务已在执行中") + try: + timeout = max(5, int(req.timeout)) + result = await run_in_threadpool( + lambda: sm.delete_accounts_batch(req.account_ids, timeout=timeout) + ) + _state.broadcast({ + "ts": datetime.now().strftime("%H:%M:%S"), + "level": "info", + "message": ( + f"[Sub2Api] 批量删除: 请求 {result.get('requested', 0)}, " + f"删除成功 {result.get('deleted_ok', 0)}, " + f"删除失败 {result.get('deleted_fail', 0)}" + ), + "step": "sub2api_accounts_delete", + }) + _clear_sub2api_accounts_cache() + return result + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + finally: + _sub2api_maintain_lock.release() + + +@app.post("/api/sub2api/accounts/handle-exception") +async def api_sub2api_accounts_handle_exception(req: Sub2ApiExceptionHandleRequest) -> Dict[str, Any]: + sm = _get_sub2api_maintainer() + if not sm: + raise HTTPException(status_code=400, detail="Sub2Api 未配置") + if not _sub2api_maintain_lock.acquire(blocking=False): + raise HTTPException(status_code=409, detail="Sub2Api 账号任务已在执行中") + try: + timeout = max(5, int(req.timeout)) + result = await run_in_threadpool( + lambda: sm.handle_exception_accounts( + req.account_ids or None, + timeout=timeout, + delete_unresolved=bool(req.delete_unresolved), + ) + ) + _state.broadcast({ + "ts": datetime.now().strftime("%H:%M:%S"), + "level": "info", + "message": ( + f"[Sub2Api] 异常处理: 目标 {result.get('targeted', 0)}, " + f"恢复 {result.get('recovered', 0)}, " + f"删除 {result.get('deleted_ok', 0)}(失败 {result.get('deleted_fail', 0)})" + ), + "step": "sub2api_accounts_exception", + }) + _clear_sub2api_accounts_cache() + return result + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + finally: + _sub2api_maintain_lock.release() + + +@app.get("/api/sub2api/pool/status") +async def api_sub2api_pool_status() -> Dict[str, Any]: + sm = _get_sub2api_maintainer() + if not sm: + return {"configured": False, "error": "Sub2Api 未配置"} + status = await run_in_threadpool(sm.get_pool_status) + status["configured"] = True + return status + + +@app.post("/api/sub2api/pool/check") +async def api_sub2api_pool_check() -> Dict[str, Any]: + sm = _get_sub2api_maintainer() + if not sm: + raise HTTPException(status_code=400, detail="Sub2Api 未配置") + result = await run_in_threadpool(sm.test_connection) + return result + + +@app.post("/api/sub2api/pool/maintain") +async def api_sub2api_pool_maintain() -> Dict[str, Any]: + sm = _get_sub2api_maintainer() + if not sm: + raise HTTPException(status_code=400, detail="Sub2Api 未配置") + if not _sub2api_maintain_lock.acquire(blocking=False): + raise HTTPException(status_code=409, detail="Sub2Api 维护任务已在执行中") + try: + cfg = _get_sync_config() + actions = _get_sub2api_maintain_actions(cfg) + result = await run_in_threadpool( + lambda: sm.probe_and_clean_sync(actions=actions) + ) + _state.broadcast({ + "ts": datetime.now().strftime("%H:%M:%S"), + "level": "info", + "message": _format_sub2api_maintain_result_message(result), + "step": "sub2api_maintain", + }) + _clear_sub2api_accounts_cache() + return result + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + finally: + _sub2api_maintain_lock.release() + + +@app.post("/api/sub2api/pool/dedupe") +async def api_sub2api_pool_dedupe(req: Sub2ApiDedupeRequest) -> Dict[str, Any]: + sm = _get_sub2api_maintainer() + if not sm: + raise HTTPException(status_code=400, detail="Sub2Api 未配置") + if not _sub2api_maintain_lock.acquire(blocking=False): + raise HTTPException(status_code=409, detail="Sub2Api 维护任务已在执行中") + try: + timeout = max(5, int(req.timeout)) + dry_run = bool(req.dry_run) + result = await run_in_threadpool( + lambda: sm.dedupe_duplicate_accounts(timeout=timeout, dry_run=dry_run) + ) + if dry_run: + _state.broadcast({ + "ts": datetime.now().strftime("%H:%M:%S"), + "level": "info", + "message": ( + f"[Sub2Api] 重复预检完成: 重复组 {result.get('duplicate_groups', 0)}, " + f"可删 {result.get('to_delete', 0)}" + ), + "step": "sub2api_dedupe", + }) + else: + _state.broadcast({ + "ts": datetime.now().strftime("%H:%M:%S"), + "level": "info", + "message": ( + f"[Sub2Api] 重复清理完成: 删除成功 {result.get('deleted_ok', 0)}, " + f"删除失败 {result.get('deleted_fail', 0)}" + ), + "step": "sub2api_dedupe", + }) + _clear_sub2api_accounts_cache() + return result + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + finally: + _sub2api_maintain_lock.release() + + +def _start_sub2api_auto_maintain() -> None: + global _sub2api_auto_maintain_thread, _sub2api_auto_maintain_stop + cfg = _get_sync_config() + interval = max(5, int(cfg.get("sub2api_maintain_interval_minutes", 30))) * 60 + with _sub2api_auto_maintain_ctl_lock: + if _sub2api_auto_maintain_thread and _sub2api_auto_maintain_thread.is_alive(): + return + stop_event = threading.Event() + _sub2api_auto_maintain_stop = stop_event + + def _loop(local_stop: threading.Event) -> None: + while not local_stop.is_set(): + sm = _get_sub2api_maintainer() + if sm: + if not _sub2api_maintain_lock.acquire(blocking=False): + _state.broadcast({ + "ts": datetime.now().strftime("%H:%M:%S"), + "level": "warn", + "message": "[Sub2Api] 跳过自动维护:已有维护任务在执行", + "step": "sub2api_auto", + }) + else: + try: + current_cfg = _get_sync_config() + result = sm.probe_and_clean_sync( + actions=_get_sub2api_maintain_actions(current_cfg) + ) + _state.broadcast({ + "ts": datetime.now().strftime("%H:%M:%S"), + "level": "info", + "message": _format_sub2api_maintain_result_message(result, auto=True), + "step": "sub2api_auto", + }) + _clear_sub2api_accounts_cache() + except Exception as e: + _state.broadcast({ + "ts": datetime.now().strftime("%H:%M:%S"), + "level": "error", + "message": f"[Sub2Api] 自动维护异常: {e}", + "step": "sub2api_auto", + }) + finally: + _sub2api_maintain_lock.release() + _try_auto_register() + local_stop.wait(interval) + + thread = threading.Thread(target=_loop, args=(stop_event,), daemon=True) + with _sub2api_auto_maintain_ctl_lock: + _sub2api_auto_maintain_thread = thread + thread.start() + + +def _stop_sub2api_auto_maintain() -> None: + global _sub2api_auto_maintain_thread, _sub2api_auto_maintain_stop + with _sub2api_auto_maintain_ctl_lock: + stop_event = _sub2api_auto_maintain_stop + thread = _sub2api_auto_maintain_thread + if stop_event: + stop_event.set() + if thread and thread.is_alive(): + thread.join(timeout=5) + with _sub2api_auto_maintain_ctl_lock: + if _sub2api_auto_maintain_thread is thread and (thread is None or not thread.is_alive()): + _sub2api_auto_maintain_thread = None + _sub2api_auto_maintain_stop = None + + +@app.on_event("startup") +async def _startup_restore_background_tasks() -> None: + _service_shutdown_event.clear() + cfg = _get_sync_config() + if cfg.get("auto_maintain"): + _start_auto_maintain() + if cfg.get("sub2api_auto_maintain"): + _start_sub2api_auto_maintain() + + +@app.on_event("shutdown") +async def _shutdown_background_tasks() -> None: + _service_shutdown_event.set() + _stop_auto_maintain() + _stop_sub2api_auto_maintain() + + +# 挂载静态文件 +app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static") + +# ========================================== +# 入口(兼容直接运行) +# ========================================== + +if __name__ == "__main__": + from .__main__ import main + main() diff --git a/openai_pool_orchestrator/static/app.js b/openai_pool_orchestrator/static/app.js new file mode 100755 index 0000000..1e3b6f0 --- /dev/null +++ b/openai_pool_orchestrator/static/app.js @@ -0,0 +1,2780 @@ +/** + * OpenAI Pool Orchestrator — v5.2.1 + */ + +// ========================================== +// 状态 +// ========================================== +const state = { + task: { + status: 'idle', + run_id: null, + revision: -1, + server_time: null, + }, + runtime: { + run_id: null, + revision: -1, + focus_worker_id: null, + workers: [], + completion_semantics: 'registration_only', + }, + stats: { + success: 0, + fail: 0, + total: 0, + }, + ui: { + autoScroll: true, + logCount: 0, + focusWorkerId: null, + focusLocked: false, + eventSource: null, + tokens: [], + tokenFilter: { + status: 'all', + keyword: '', + }, + sub2apiAccounts: [], + sub2apiAccountFilter: { + status: 'all', + keyword: '', + }, + sub2apiAccountPager: { + page: 1, + pageSize: 20, + total: 0, + filteredTotal: 0, + totalPages: 1, + }, + selectedSub2ApiAccountIds: new Set(), + sub2apiAccountsLoading: false, + sub2apiAccountActionBusy: false, + countdownTimer: null, + _loadTokensTimer: null, + latestRevisionByRun: {}, + snapshotRequested: false, + dataPanelTab: 'dataPanelSub2Api', + }, +}; + +// ========================================== +// DOM 引用 +// ========================================== +const $ = id => document.getElementById(id); +const DOM = {}; + +const STEP_DISPLAY_LABELS = { + check_proxy: '网络检查', + create_email: '创建邮箱', + oauth_init: 'OAuth 初始化', + sentinel: 'Sentinel Token', + signup: '提交注册', + send_otp: '发送验证码', + wait_otp: '等待验证码', + verify_otp: '验证 OTP', + create_account: '创建账户', + workspace: '选择 Workspace', + get_token: '获取 Token', + start: '开始新一轮', + saved: '保存 Token', + retry: '等待重试', + runtime: '运行异常', + wait: '等待下一轮', + stopped: '已停止', + dedupe: '重复检测', + sync: '同步 Sub2Api', + cpa_upload: '上传 CPA', + mode: '上传策略', + auto_stop: '自动停止', + stopping: '停止中', +}; + +const STATUS_LABEL_MAP = { + idle: '空闲', + starting: '启动中', + preparing: '准备中', + running: '运行中', + registering: '注册中', + postprocessing: '后处理中', + waiting: '等待中', + stopping: '停止中', + stopped: '已停止', + finished: '已完成', + failed: '失败', + error: '异常', +}; + +const PHASE_LABEL_MAP = { + preparing: '准备阶段', + registration: '注册阶段', + postprocess: '后处理阶段', + finished: '结束阶段', + idle: '等待任务', +}; + +const COMPLETION_SEMANTICS_MAP = { + registration_only: '注册完成即结束', + requires_postprocess: '注册完成后仍需后处理', +}; + +const WORKER_STATUS_PRIORITY = { + registering: 6, + postprocessing: 5, + preparing: 4, + running: 4, + waiting: 3, + error: 3, + stopping: 2, + stopped: 1, + idle: 0, +}; + +const SUB2API_ABNORMAL_STATUSES = new Set(['error', 'disabled']); + +function clearSub2ApiAccountKeywordInput() { + if (!DOM.sub2apiAccountKeyword) return; + if (state.ui.sub2apiAccountFilter.keyword) return; + DOM.sub2apiAccountKeyword.value = ''; + requestAnimationFrame(() => { + if (!state.ui.sub2apiAccountFilter.keyword && DOM.sub2apiAccountKeyword) DOM.sub2apiAccountKeyword.value = ''; + }); + setTimeout(() => { + if (!state.ui.sub2apiAccountFilter.keyword && DOM.sub2apiAccountKeyword) DOM.sub2apiAccountKeyword.value = ''; + }, 120); +} + +// ========================================== +// 初始化 +// ========================================== +document.addEventListener('DOMContentLoaded', () => { + Object.assign(DOM, { + statusBadge: $('statusBadge'), + statusText: $('statusText'), + statusDot: $('statusDot'), + proxyInput: $('proxyInput'), + checkProxyBtn: $('checkProxyBtn'), + proxyStatus: $('proxyStatus'), + btnStart: $('btnStart'), + btnStop: $('btnStop'), + statSuccess: $('statSuccess'), + statFail: $('statFail'), + statTotal: $('statTotal'), + logBody: $('logBody'), + logCount: $('logCount'), + clearLogBtn: $('clearLogBtn'), + progressFill: $('progressFill'), + taskOverview: $('taskOverview'), + workerList: $('workerList'), + workerDetail: $('workerDetail'), + unlockFocusBtn: $('unlockFocusBtn'), + segmentIndicator: $('segmentIndicator'), + autoScrollCheck: $('autoScrollCheck'), + multithreadCheck: $('multithreadCheck'), + threadCountInput: $('threadCountInput'), + sub2apiBaseUrl: $('sub2apiBaseUrl'), + sub2apiEmail: $('sub2apiEmail'), + sub2apiPassword: $('sub2apiPassword'), + autoSyncCheck: $('autoSyncCheck'), + uploadMode: $('uploadMode'), + uploadModeSaveBtn: $('uploadModeSaveBtn'), + uploadModeStatus: $('uploadModeStatus'), + saveSyncConfigBtn: $('saveSyncConfigBtn'), + syncStatus: $('syncStatus'), + headerSub2apiChip: $('headerSub2apiChip'), + headerSub2apiLabel: $('headerSub2apiLabel'), + headerSub2apiDelta: $('headerSub2apiDelta'), + headerSub2apiBar: $('headerSub2apiBar'), + headerCpaChip: $('headerCpaChip'), + headerCpaLabel: $('headerCpaLabel'), + headerCpaDelta: $('headerCpaDelta'), + headerCpaBar: $('headerCpaBar'), + headerLocalTokenChip: $('headerLocalTokenChip'), + headerLocalTokenLabel: $('headerLocalTokenLabel'), + headerLocalTokenDelta: $('headerLocalTokenDelta'), + headerLocalTokenBar: $('headerLocalTokenBar'), + themeToggleBtn: $('themeToggleBtn'), + cpaBaseUrl: $('cpaBaseUrl'), + cpaToken: $('cpaToken'), + cpaMinCandidates: $('cpaMinCandidates'), + cpaUsedPercent: $('cpaUsedPercent'), + cpaAutoMaintain: $('cpaAutoMaintain'), + cpaInterval: $('cpaInterval'), + cpaTestBtn: $('cpaTestBtn'), + cpaSaveBtn: $('cpaSaveBtn'), + cpaStatus: $('cpaStatus'), + mailStrategySelect: $('mailStrategySelect'), + mailTestBtn: $('mailTestBtn'), + mailSaveBtn: $('mailSaveBtn'), + mailStatus: $('mailStatus'), + poolTotal: $('poolTotal'), + poolCandidates: $('poolCandidates'), + poolError: $('poolError'), + poolThreshold: $('poolThreshold'), + poolPercent: $('poolPercent'), + poolRefreshBtn: $('poolRefreshBtn'), + poolMaintainBtn: $('poolMaintainBtn'), + poolMaintainStatus: $('poolMaintainStatus'), + dataPanelSub2Api: $('dataPanelSub2Api'), + dataPanelCpa: $('dataPanelCpa'), + dataPanelLocalTokens: $('dataPanelLocalTokens'), + poolTokenList: $('poolTokenList'), + poolCopyRtBtn: $('poolCopyRtBtn'), + poolExportBtn: $('poolExportBtn'), + poolPwSyncBtn: $('poolPwSyncBtn'), + tokenFilterStatus: $('tokenFilterStatus'), + tokenFilterKeyword: $('tokenFilterKeyword'), + tokenFilterApplyBtn: $('tokenFilterApplyBtn'), + tokenFilterResetBtn: $('tokenFilterResetBtn'), + sub2apiPoolTotal: $('sub2apiPoolTotal'), + sub2apiPoolNormal: $('sub2apiPoolNormal'), + sub2apiPoolError: $('sub2apiPoolError'), + sub2apiPoolThreshold: $('sub2apiPoolThreshold'), + sub2apiPoolPercent: $('sub2apiPoolPercent'), + sub2apiPoolRefreshBtn: $('sub2apiPoolRefreshBtn'), + sub2apiPoolMaintainBtn: $('sub2apiPoolMaintainBtn'), + sub2apiPoolMaintainStatus: $('sub2apiPoolMaintainStatus'), + sub2apiAccountStatusFilter: $('sub2apiAccountStatusFilter'), + sub2apiAccountKeyword: $('sub2apiAccountKeyword'), + sub2apiAccountApplyBtn: $('sub2apiAccountApplyBtn'), + sub2apiAccountResetBtn: $('sub2apiAccountResetBtn'), + sub2apiAccountSelectAll: $('sub2apiAccountSelectAll'), + sub2apiAccountSelection: $('sub2apiAccountSelection'), + sub2apiAccountProbeBtn: $('sub2apiAccountProbeBtn'), + sub2apiAccountExceptionBtn: $('sub2apiAccountExceptionBtn'), + sub2apiDuplicateScanBtn: $('sub2apiDuplicateScanBtn'), + sub2apiDuplicateCleanBtn: $('sub2apiDuplicateCleanBtn'), + sub2apiAccountDeleteBtn: $('sub2apiAccountDeleteBtn'), + sub2apiAccountList: $('sub2apiAccountList'), + sub2apiAccountActionStatus: $('sub2apiAccountActionStatus'), + sub2apiAccountPrevBtn: $('sub2apiAccountPrevBtn'), + sub2apiAccountNextBtn: $('sub2apiAccountNextBtn'), + sub2apiAccountPageInfo: $('sub2apiAccountPageInfo'), + sub2apiAccountPageSize: $('sub2apiAccountPageSize'), + sub2apiMinCandidates: $('sub2apiMinCandidates'), + sub2apiInterval: $('sub2apiInterval'), + sub2apiAutoMaintain: $('sub2apiAutoMaintain'), + sub2apiTestPoolBtn: $('sub2apiTestPoolBtn'), + sub2apiMaintainRefreshAbnormal: $('sub2apiMaintainRefreshAbnormal'), + sub2apiMaintainDeleteAbnormal: $('sub2apiMaintainDeleteAbnormal'), + sub2apiMaintainDedupe: $('sub2apiMaintainDedupe'), + proxyPoolEnabled: $('proxyPoolEnabled'), + proxyPoolApiUrl: $('proxyPoolApiUrl'), + proxyPoolAuthMode: $('proxyPoolAuthMode'), + proxyPoolApiKey: $('proxyPoolApiKey'), + proxyPoolCount: $('proxyPoolCount'), + proxyPoolCountry: $('proxyPoolCountry'), + proxyPoolTestBtn: $('proxyPoolTestBtn'), + proxyPoolSaveBtn: $('proxyPoolSaveBtn'), + proxyPoolStatus: $('proxyPoolStatus'), + saveProxyBtn: $('saveProxyBtn'), + autoRegisterCheck: $('autoRegisterCheck'), + }); + + clearSub2ApiAccountKeywordInput(); + + renderRuntimePanels(); + connectSSE(); + loadTokens(); + requestStatusSnapshot(); + loadSyncConfig(); + loadProxyPoolConfig(); + loadPoolConfig(); + loadMailConfig(); + initMailCheckboxes(); + pollPoolStatus(); + pollSub2ApiPoolStatus(); + loadSub2ApiAccounts(); + initThemeSwitch(); + initCollapsibles(); + initDataPanelTabs(); + + DOM.checkProxyBtn.addEventListener('click', checkProxy); + if (DOM.saveProxyBtn) DOM.saveProxyBtn.addEventListener('click', saveProxy); + DOM.btnStart.addEventListener('click', startTask); + DOM.btnStop.addEventListener('click', stopTask); + DOM.clearLogBtn.addEventListener('click', clearLog); + if (DOM.unlockFocusBtn) DOM.unlockFocusBtn.addEventListener('click', unlockFocusWorker); + + DOM.saveSyncConfigBtn.addEventListener('click', saveSyncConfig); + if (DOM.uploadModeSaveBtn) DOM.uploadModeSaveBtn.addEventListener('click', saveUploadMode); + DOM.cpaTestBtn.addEventListener('click', testCpaConnection); + DOM.cpaSaveBtn.addEventListener('click', savePoolConfig); + DOM.mailTestBtn.addEventListener('click', testMailConnection); + DOM.mailSaveBtn.addEventListener('click', saveMailConfig); + + DOM.poolRefreshBtn.addEventListener('click', pollPoolStatus); + DOM.poolMaintainBtn.addEventListener('click', triggerMaintenance); + if (DOM.poolCopyRtBtn) DOM.poolCopyRtBtn.addEventListener('click', copyAllRt); + if (DOM.poolExportBtn) DOM.poolExportBtn.addEventListener('click', exportLocalTokens); + if (DOM.poolPwSyncBtn) DOM.poolPwSyncBtn.addEventListener('click', batchSync); + if (DOM.tokenFilterApplyBtn) DOM.tokenFilterApplyBtn.addEventListener('click', applyTokenFilter); + if (DOM.tokenFilterResetBtn) DOM.tokenFilterResetBtn.addEventListener('click', resetTokenFilter); + if (DOM.tokenFilterKeyword) { + DOM.tokenFilterKeyword.addEventListener('keydown', (e) => { + if (e.key === 'Enter') applyTokenFilter(); + }); + } + if (DOM.sub2apiPoolRefreshBtn) { + DOM.sub2apiPoolRefreshBtn.addEventListener('click', () => { + pollSub2ApiPoolStatus(); + loadSub2ApiAccounts(); + }); + } + if (DOM.sub2apiPoolMaintainBtn) DOM.sub2apiPoolMaintainBtn.addEventListener('click', triggerSub2ApiMaintenance); + if (DOM.sub2apiTestPoolBtn) DOM.sub2apiTestPoolBtn.addEventListener('click', testSub2ApiPoolConnection); + if (DOM.sub2apiAccountApplyBtn) DOM.sub2apiAccountApplyBtn.addEventListener('click', applySub2ApiAccountFilter); + if (DOM.sub2apiAccountResetBtn) DOM.sub2apiAccountResetBtn.addEventListener('click', resetSub2ApiAccountFilter); + if (DOM.sub2apiAccountKeyword) { + DOM.sub2apiAccountKeyword.addEventListener('keydown', (e) => { + if (e.key === 'Enter') applySub2ApiAccountFilter(); + }); + } + + window.addEventListener('pageshow', () => { + clearSub2ApiAccountKeywordInput(); + }); + if (DOM.sub2apiAccountPrevBtn) DOM.sub2apiAccountPrevBtn.addEventListener('click', () => changeSub2ApiAccountPage(-1)); + if (DOM.sub2apiAccountNextBtn) DOM.sub2apiAccountNextBtn.addEventListener('click', () => changeSub2ApiAccountPage(1)); + if (DOM.sub2apiAccountPageSize) { + DOM.sub2apiAccountPageSize.addEventListener('change', () => changeSub2ApiAccountPageSize()); + } + if (DOM.sub2apiAccountSelectAll) DOM.sub2apiAccountSelectAll.addEventListener('change', toggleSelectAllSub2ApiAccounts); + if (DOM.sub2apiAccountProbeBtn) DOM.sub2apiAccountProbeBtn.addEventListener('click', triggerSelectedSub2ApiProbe); + if (DOM.sub2apiAccountExceptionBtn) DOM.sub2apiAccountExceptionBtn.addEventListener('click', triggerSub2ApiExceptionHandling); + if (DOM.sub2apiDuplicateScanBtn) DOM.sub2apiDuplicateScanBtn.addEventListener('click', previewSub2ApiDuplicates); + if (DOM.sub2apiDuplicateCleanBtn) DOM.sub2apiDuplicateCleanBtn.addEventListener('click', cleanupSub2ApiDuplicates); + if (DOM.sub2apiAccountDeleteBtn) DOM.sub2apiAccountDeleteBtn.addEventListener('click', triggerSelectedSub2ApiDelete); + if (DOM.proxyPoolTestBtn) DOM.proxyPoolTestBtn.addEventListener('click', testProxyPoolFetch); + if (DOM.proxyPoolSaveBtn) DOM.proxyPoolSaveBtn.addEventListener('click', saveProxyPoolConfig); + + if (DOM.poolTokenList) { + DOM.poolTokenList.addEventListener('click', async (e) => { + const copyBtn = e.target.closest('.token-copy-btn'); + if (copyBtn) { + try { + const payload = decodeURIComponent(copyBtn.dataset.payload || ''); + await copyToken(payload); + } catch { showToast('复制失败', 'error'); } + return; + } + const deleteBtn = e.target.closest('.token-delete-btn'); + if (deleteBtn) { + const filename = decodeURIComponent(deleteBtn.dataset.filename || ''); + if (filename) deleteToken(filename); + } + }); + } + + if (DOM.sub2apiAccountList) { + DOM.sub2apiAccountList.addEventListener('click', async (e) => { + const probeBtn = e.target.closest('.sub2api-account-probe-btn'); + if (probeBtn) { + const accountId = parseInt(probeBtn.dataset.accountId, 10); + if (Number.isInteger(accountId) && accountId > 0) { + await runSub2ApiAccountProbe([accountId], `账号 ${accountId}`); + } + return; + } + const deleteBtn = e.target.closest('.sub2api-account-delete-btn'); + if (deleteBtn) { + const accountId = parseInt(deleteBtn.dataset.accountId, 10); + const email = decodeURIComponent(deleteBtn.dataset.email || ''); + if (Number.isInteger(accountId) && accountId > 0) { + await runSub2ApiAccountDelete([accountId], email || `账号 ${accountId}`); + } + } + }); + DOM.sub2apiAccountList.addEventListener('change', (e) => { + const checkbox = e.target.closest('.sub2api-account-check'); + if (!checkbox) return; + const accountId = parseInt(checkbox.dataset.accountId, 10); + if (!Number.isInteger(accountId) || accountId <= 0) return; + if (checkbox.checked) state.ui.selectedSub2ApiAccountIds.add(accountId); + else state.ui.selectedSub2ApiAccountIds.delete(accountId); + const row = checkbox.closest('.sub2api-account-item'); + if (row) row.classList.toggle('selected', checkbox.checked); + refreshSub2ApiSelectionState(); + }); + } + + DOM.logBody.addEventListener('scroll', () => { + const el = DOM.logBody; + const isAtBottom = (el.scrollTop + el.clientHeight >= el.scrollHeight - 20); + state.ui.autoScroll = isAtBottom; + if (DOM.autoScrollCheck) DOM.autoScrollCheck.checked = isAtBottom; + }); + + if (DOM.autoScrollCheck) { + DOM.autoScrollCheck.checked = state.ui.autoScroll; + DOM.autoScrollCheck.addEventListener('change', () => { + state.ui.autoScroll = DOM.autoScrollCheck.checked; + if (state.ui.autoScroll) DOM.logBody.scrollTop = DOM.logBody.scrollHeight; + }); + } + + setInterval(requestStatusSnapshot, 5000); + setInterval(loadTokens, 60000); + setInterval(pollPoolStatus, 30000); + setInterval(pollSub2ApiPoolStatus, 30000); + setInterval(() => loadSub2ApiAccounts({ silent: true }), 60000); + + initTabs(); +}); + +// ========================================== +// Tab 导航切换 — iOS Segmented Control +// ========================================== +function initTabs() { + const tabBtns = document.querySelectorAll('.tab-btn'); + if (!tabBtns.length) return; + + tabBtns.forEach((btn, index) => { + btn.addEventListener('click', () => { + switchMainTab(btn.dataset.tab || 'tabDashboard'); + }); + }); + + const activeTab = Array.from(tabBtns).find(btn => btn.classList.contains('active'))?.dataset.tab || 'tabDashboard'; + switchMainTab(activeTab); +} + +function switchMainTab(tabId) { + const nextTab = tabId === 'tabConfig' ? 'tabConfig' : 'tabDashboard'; + const tabBtns = document.querySelectorAll('.tab-btn'); + const tabPanels = document.querySelectorAll('.tab-panel'); + + tabBtns.forEach((btn, index) => { + const active = btn.dataset.tab === nextTab; + btn.classList.toggle('active', active); + btn.setAttribute('aria-selected', active ? 'true' : 'false'); + if (active && DOM.segmentIndicator) { + DOM.segmentIndicator.setAttribute('data-active', String(index)); + } + }); + + tabPanels.forEach((panel) => { + panel.classList.toggle('active', panel.id === nextTab); + }); +} + +function initDataPanelTabs() { + const defaultTab = 'dataPanelSub2Api'; + const tabButtons = [DOM.headerSub2apiChip, DOM.headerCpaChip, DOM.headerLocalTokenChip].filter(Boolean); + if (!tabButtons.length) return; + + tabButtons.forEach((btn, index) => { + btn.addEventListener('click', () => { + switchDataPanelTab(btn.dataset.panelTab || defaultTab); + }); + + btn.addEventListener('keydown', (event) => { + if (!['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(event.key)) return; + event.preventDefault(); + + let nextIndex = index; + if (event.key === 'ArrowRight') nextIndex = (index + 1) % tabButtons.length; + if (event.key === 'ArrowLeft') nextIndex = (index - 1 + tabButtons.length) % tabButtons.length; + if (event.key === 'Home') nextIndex = 0; + if (event.key === 'End') nextIndex = tabButtons.length - 1; + + const targetBtn = tabButtons[nextIndex]; + if (!targetBtn) return; + targetBtn.focus(); + switchDataPanelTab(targetBtn.dataset.panelTab || defaultTab); + }); + }); + + if (DOM.headerSub2apiChip) DOM.headerSub2apiChip.dataset.panelTab = 'dataPanelSub2Api'; + if (DOM.headerCpaChip) DOM.headerCpaChip.dataset.panelTab = 'dataPanelCpa'; + if (DOM.headerLocalTokenChip) DOM.headerLocalTokenChip.dataset.panelTab = 'dataPanelLocalTokens'; + + switchDataPanelTab(state.ui.dataPanelTab || defaultTab); +} + +function switchDataPanelTab(tabId) { + const nextTab = ['dataPanelSub2Api', 'dataPanelCpa', 'dataPanelLocalTokens'].includes(tabId) ? tabId : 'dataPanelSub2Api'; + state.ui.dataPanelTab = nextTab; + + const panelMap = { + dataPanelSub2Api: DOM.dataPanelSub2Api, + dataPanelCpa: DOM.dataPanelCpa, + dataPanelLocalTokens: DOM.dataPanelLocalTokens, + }; + const buttonMap = { + dataPanelSub2Api: DOM.headerSub2apiChip, + dataPanelCpa: DOM.headerCpaChip, + dataPanelLocalTokens: DOM.headerLocalTokenChip, + }; + + Object.entries(panelMap).forEach(([id, panel]) => { + if (!panel) return; + panel.classList.toggle('active', id === nextTab); + }); + Object.entries(buttonMap).forEach(([id, btn]) => { + if (!btn) return; + const active = id === nextTab; + btn.classList.toggle('active-view', active); + btn.setAttribute('aria-pressed', active ? 'true' : 'false'); + btn.tabIndex = active ? 0 : -1; + }); + + const dashboardActive = document.getElementById('tabDashboard')?.classList.contains('active'); + if (!dashboardActive) { + switchMainTab('tabDashboard'); + } +} + +// ========================================== +// 折叠面板 +// ========================================== +function initCollapsibles() { + document.querySelectorAll('.collapsible-trigger').forEach(trigger => { + trigger.addEventListener('click', () => { + const section = trigger.closest('.collapsible'); + if (!section) return; + const body = section.querySelector('.collapsible-body'); + if (!body) return; + const icon = trigger.querySelector('.collapse-icon'); + const isOpen = section.classList.contains('open'); + if (isOpen) { + section.classList.remove('open'); + body.style.display = 'none'; + if (icon) icon.classList.remove('open'); + } else { + section.classList.add('open'); + body.style.display = 'block'; + if (icon) icon.classList.add('open'); + } + }); + }); +} + +// ========================================== +// SSE / 快照同步 +// ========================================== +function connectSSE() { + if (state.ui.eventSource) state.ui.eventSource.close(); + const es = new EventSource('/api/logs'); + state.ui.eventSource = es; + + const handleEvent = (sourceType, raw) => { + try { + const payload = raw?.data ? JSON.parse(raw.data) : {}; + const event = payload && typeof payload === 'object' ? { ...payload } : {}; + if (!event.type && sourceType && sourceType !== 'message') event.type = sourceType; + if (!event.type && event.event) event.type = event.event; + + if (event.type) { + applySseEvent(event); + return; + } + + if (Object.prototype.hasOwnProperty.call(event, 'task') + || Object.prototype.hasOwnProperty.call(event, 'runtime') + || Object.prototype.hasOwnProperty.call(event, 'stats')) { + applyStatusSnapshot(event); + } + } catch { } + }; + + ['connected', 'snapshot', 'task.updated', 'worker.updated', 'worker.step.updated', 'stats.updated', 'log.appended', 'task.finished'] + .forEach((eventName) => { + es.addEventListener(eventName, (e) => handleEvent(eventName, e)); + }); + + es.onmessage = (e) => handleEvent('message', e); + es.onerror = () => setTimeout(connectSSE, 3000); +} + +// ========================================== +// 日志渲染 +// ========================================== +const LEVEL_ICON = { info: '›', success: '✓', error: '✗', warn: '⚠', connected: '⟳' }; + +function appendLog(event) { + const { ts, level, message, step } = event; + state.ui.logCount++; + const entry = document.createElement('div'); + entry.className = 'log-entry'; + entry.innerHTML = ` + ${escapeHtml(ts || '')} + ${LEVEL_ICON[level] || '·'} + ${escapeHtml(message || '')} + ${step ? `${escapeHtml(getStepDisplayLabel(step))}` : ''} + `; + DOM.logBody.appendChild(entry); + DOM.logCount.textContent = state.ui.logCount; + if (state.ui.autoScroll) DOM.logBody.scrollTop = DOM.logBody.scrollHeight; + if (DOM.logBody.children.length > 2000) { + DOM.logBody.firstElementChild.remove(); + } +} + +function clearLog() { + DOM.logBody.innerHTML = ''; + state.ui.logCount = 0; + DOM.logCount.textContent = '0'; +} + +function normalizeRevision(value, fallback = -1) { + const num = Number(value); + return Number.isFinite(num) ? num : fallback; +} + +function normalizeRunId(runId) { + if (runId === null || runId === undefined || runId === '') return null; + const value = String(runId).trim(); + return value || null; +} + +function normalizeWorkerId(workerId) { + if (workerId === null || workerId === undefined || workerId === '') return null; + const value = String(workerId).trim(); + return value || null; +} + +function normalizeTaskSnapshot(task, serverTime = null) { + const source = task && typeof task === 'object' ? task : {}; + return { + ...state.task, + ...source, + status: source.status || 'idle', + run_id: normalizeRunId(source.run_id) || null, + revision: normalizeRevision(source.revision, state.task.revision), + server_time: serverTime || source.server_time || state.task.server_time || null, + }; +} + +function normalizeStatsSnapshot(stats) { + const source = stats && typeof stats === 'object' ? stats : {}; + const success = Number(source.success || 0); + const fail = Number(source.fail || 0); + const total = Number.isFinite(Number(source.total)) ? Number(source.total) : (success + fail); + return { + ...state.stats, + ...source, + success, + fail, + total, + }; +} + +function normalizeWorkerStep(step, fallbackIndex = 0) { + if (!step) return null; + + const id = String(step.id || step.step_id || step.step || '').trim(); + if (!id) return null; + const rawStatus = String(step.status || step.state || 'pending').toLowerCase(); + let status = rawStatus; + if (['done', 'completed', 'ok'].includes(rawStatus)) status = 'done'; + else if (['error', 'failed', 'fail'].includes(rawStatus)) status = 'error'; + else if (['active', 'running', 'in_progress'].includes(rawStatus)) status = 'active'; + else if (['skipped'].includes(rawStatus)) status = 'skipped'; + else status = 'pending'; + + return { + ...step, + id, + step_id: step.step_id || id, + label: step.label || id, + status, + message: step.message || '', + index: Number.isFinite(Number(step.index)) ? Number(step.index) : fallbackIndex, + started_at: step.started_at || '', + finished_at: step.finished_at || '', + updated_at: step.updated_at || step.finished_at || step.started_at || '', + }; +} + +const MAX_WORKER_STEP_ITEMS = 16; + +function normalizeWorkerSteps(steps) { + const normalized = Array.isArray(steps) + ? steps + .map((step, index) => normalizeWorkerStep(step, index)) + .filter(Boolean) + : (steps && typeof steps === 'object') + ? Object.entries(steps) + .map(([id, status], index) => normalizeWorkerStep({ id, status, index }, index)) + .filter(Boolean) + : []; + + if (!normalized.length) return []; + + const deduped = new Map(); + normalized.forEach((step, index) => { + const key = String(step.step_id || step.id || '').trim(); + if (!key) return; + + const normalizedIndex = Number.isFinite(Number(step.index)) ? Number(step.index) : index; + const nextStep = { ...step, step_id: key, id: key, index: normalizedIndex }; + const previous = deduped.get(key); + if (!previous) { + deduped.set(key, nextStep); + return; + } + + const previousUpdated = String(previous.updated_at || previous.finished_at || previous.started_at || ''); + const nextUpdated = String(nextStep.updated_at || nextStep.finished_at || nextStep.started_at || ''); + if (nextUpdated >= previousUpdated) { + deduped.set(key, { ...previous, ...nextStep, index: Math.min(previous.index, normalizedIndex) }); + } + }); + + return [...deduped.values()] + .sort((a, b) => { + const ai = Number.isFinite(a.index) ? a.index : Number.MAX_SAFE_INTEGER; + const bi = Number.isFinite(b.index) ? b.index : Number.MAX_SAFE_INTEGER; + if (ai !== bi) return ai - bi; + return String(a.updated_at || '').localeCompare(String(b.updated_at || '')); + }) + .slice(-MAX_WORKER_STEP_ITEMS); +} + +function normalizeWorker(worker, fallbackId = null) { + const source = worker && typeof worker === 'object' ? worker : {}; + const workerId = normalizeWorkerId(source.worker_id ?? fallbackId); + if (!workerId) return null; + + return { + ...source, + worker_id: workerId, + worker_label: source.worker_label || `W${workerId}`, + status: source.status || 'idle', + phase: source.phase || 'idle', + revision: normalizeRevision(source.revision ?? source.runtime_revision, -1), + current_step: source.current_step || '', + message: source.message || '', + email: source.email || source.account_email || '', + mail_provider: source.mail_provider || '', + updated_at: source.updated_at || source.ts || '', + steps: normalizeWorkerSteps(source.steps), + }; +} + +function normalizeRuntimeSnapshot(runtime, taskRunId = null) { + const source = runtime && typeof runtime === 'object' ? runtime : {}; + const workers = Array.isArray(source.workers) + ? source.workers.map(worker => normalizeWorker(worker)).filter(Boolean) + : Object.entries(source.workers || {}).map(([workerId, worker]) => normalizeWorker(worker, workerId)).filter(Boolean); + + return { + ...state.runtime, + ...source, + run_id: normalizeRunId(source.run_id) || taskRunId || null, + revision: normalizeRevision(source.revision, state.runtime.revision), + focus_worker_id: normalizeWorkerId(source.focus_worker_id), + completion_semantics: source.completion_semantics || state.runtime.completion_semantics || 'registration_only', + workers, + }; +} + +function getKnownRevision(runId) { + const key = normalizeRunId(runId); + if (!key) return -1; + return normalizeRevision(state.ui.latestRevisionByRun[key], -1); +} + +function rememberRevision(runId, revision) { + const key = normalizeRunId(runId); + if (!key || !Number.isFinite(revision)) return; + state.ui.latestRevisionByRun[key] = Math.max(getKnownRevision(key), revision); +} + +function shouldIgnoreEvent(runId, revision) { + const key = normalizeRunId(runId) || normalizeRunId(state.task.run_id) || normalizeRunId(state.runtime.run_id); + if (!key || !Number.isFinite(revision)) return false; + const known = getKnownRevision(key); + if (known >= 0 && revision < known) return true; + if (known >= 0 && revision > known + 1) requestStatusSnapshot(); + rememberRevision(key, revision); + return false; +} + +function requestStatusSnapshot() { + if (state.ui.snapshotRequested) return; + state.ui.snapshotRequested = true; + fetch('/api/status') + .then(res => res.json()) + .then(payload => applyStatusSnapshot(payload, { force: true })) + .catch(() => {}) + .finally(() => { + state.ui.snapshotRequested = false; + }); +} + +function applyStatusSnapshot(payload, { force = false } = {}) { + if (!payload || typeof payload !== 'object') return; + + const nextTask = normalizeTaskSnapshot(payload.task, payload.server_time || null); + const nextRuntime = normalizeRuntimeSnapshot(payload.runtime, nextTask.run_id); + const snapshotRevision = Math.max(nextTask.revision, nextRuntime.revision); + const snapshotRunId = normalizeRunId(nextTask.run_id) || normalizeRunId(nextRuntime.run_id); + + if (!force && shouldIgnoreEvent(snapshotRunId, snapshotRevision)) return; + rememberRevision(snapshotRunId, snapshotRevision); + + state.task = nextTask; + state.runtime = nextRuntime; + state.stats = normalizeStatsSnapshot(payload.stats); + + ensureFocusWorker(); + syncTaskChrome(); + renderRuntimePanels(); +} + +function applySseEvent(event) { + if (!event || typeof event !== 'object') return; + const type = String(event.type || event.event || '').trim(); + const runId = normalizeRunId(event.run_id || event.task?.run_id || event.runtime?.run_id || event.worker?.run_id); + const revision = normalizeRevision(event.revision ?? event.task?.revision ?? event.runtime?.revision ?? event.worker?.revision, NaN); + + if (type && shouldIgnoreEvent(runId, revision)) return; + + if (type === 'connected') { + appendLog({ ts: event.ts || '', level: 'connected', message: event.message || '实时事件已连接' }); + if (event.snapshot) applyStatusSnapshot(event.snapshot, { force: true }); + else requestStatusSnapshot(); + return; + } + + if (type === 'snapshot') { + applyStatusSnapshot(event.snapshot || event.payload || event, { force: true }); + return; + } + + if (type === 'log.appended') { + const logEvent = event.log && typeof event.log === 'object' ? event.log : event; + appendLog(logEvent); + if (logEvent.level === 'token_saved') { + debouncedLoadTokens(); + showToast('新 Token 已保存: ' + (logEvent.message || ''), 'success'); + } + if (logEvent.level === 'sync_ok') { + showToast('已自动同步: ' + (logEvent.message || ''), 'success'); + } + if (logEvent.step === 'wait' && logEvent.message) { + const match = String(logEvent.message).match(/(\d+)\s*秒/); + if (match) startCountdown(parseInt(match[1], 10)); + } + return; + } + + if (type === 'task.updated' || type === 'task.finished') { + state.task = normalizeTaskSnapshot({ ...state.task, ...(event.task || event) }, event.server_time || state.task.server_time); + if (type === 'task.finished') requestStatusSnapshot(); + syncTaskChrome(); + renderRuntimePanels(); + return; + } + + if (type === 'stats.updated') { + state.stats = normalizeStatsSnapshot({ ...state.stats, ...(event.stats || event) }); + syncTaskChrome(); + renderRuntimePanels(); + return; + } + + if (type === 'worker.updated') { + mergeWorkerIntoRuntime(event.worker || event.runtime || event); + return; + } + + if (type === 'worker.step.updated') { + mergeWorkerStepUpdate(event); + return; + } + + if (Object.prototype.hasOwnProperty.call(event, 'task') + || Object.prototype.hasOwnProperty.call(event, 'runtime') + || Object.prototype.hasOwnProperty.call(event, 'stats')) { + applyStatusSnapshot(event); + } +} + +// ========================================== +// 代理检测 +// ========================================== +async function checkProxy() { + const proxy = DOM.proxyInput.value.trim(); + if (!proxy) { showToast('请先填写代理地址', 'error'); return; } + DOM.proxyStatus.className = 'proxy-status'; + DOM.proxyStatus.innerHTML = '检测中...'; + DOM.checkProxyBtn.disabled = true; + try { + const res = await fetch('/api/check-proxy', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ proxy }), + }); + const data = await res.json(); + if (data.ok) { + DOM.proxyStatus.className = 'proxy-status ok'; + DOM.proxyStatus.innerHTML = `可用 · 所在地: ${escapeHtml(data.loc || '')}`; + } else { + DOM.proxyStatus.className = 'proxy-status fail'; + DOM.proxyStatus.innerHTML = `不可用 · ${escapeHtml(data.error || '')}`; + } + } catch { + DOM.proxyStatus.className = 'proxy-status fail'; + DOM.proxyStatus.innerHTML = '检测请求失败'; + } finally { + DOM.checkProxyBtn.disabled = false; + } +} + +// ========================================== +// 代理保存 +// ========================================== +async function saveProxy() { + const proxy = DOM.proxyInput.value.trim(); + const auto_register = DOM.autoRegisterCheck ? DOM.autoRegisterCheck.checked : false; + try { + const res = await fetch('/api/proxy/save', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ proxy, auto_register }), + }); + if (res.ok) { + showToast('代理配置已保存', 'success'); + } else { + showToast('保存失败', 'error'); + } + } catch (e) { + showToast('保存请求失败: ' + e.message, 'error'); + } +} + +// ========================================== +// 启动 / 停止任务 +// ========================================== +function getRequestedWorkerCount() { + const multithread = DOM.multithreadCheck ? DOM.multithreadCheck.checked : false; + if (!multithread) return 1; + return Math.max(1, DOM.threadCountInput ? (parseInt(DOM.threadCountInput.value, 10) || 1) : 1); +} + +async function startTask() { + const proxy = DOM.proxyInput.value.trim(); + const worker_count = getRequestedWorkerCount(); + try { + const res = await fetch('/api/start', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ proxy, worker_count }), + }); + const data = await res.json(); + if (!res.ok) { + showToast(data.detail || '启动失败', 'error'); + return; + } + applyStatusSnapshot(data, { force: true }); + const workerMsg = worker_count > 1 ? ` (${worker_count} 线程)` : ''; + showToast('注册任务已启动' + workerMsg, 'success'); + } catch (e) { + showToast('启动请求失败: ' + e.message, 'error'); + } +} + +async function stopTask() { + try { + const res = await fetch('/api/stop', { method: 'POST' }); + const data = await res.json(); + if (!res.ok) { + showToast(data.detail || '停止失败', 'error'); + return; + } + applyStatusSnapshot(data, { force: true }); + showToast('正在停止任务...', 'info'); + requestStatusSnapshot(); + } catch (e) { + showToast('停止请求失败: ' + e.message, 'error'); + } +} + +// ========================================== +// 状态更新 / 渲染 +// ========================================== +function syncTaskChrome() { + const status = state.task.status || 'idle'; + DOM.statusBadge.className = `status-badge ${status}`; + DOM.statusText.textContent = formatTaskStatusLabel(status); + + const hasLiveRun = Boolean(state.task.run_id) && !state.task.finished_at; + const isActive = ['starting', 'running'].includes(status); + const isStopping = status === 'stopping'; + const canStop = hasLiveRun && ['starting', 'running', 'failed'].includes(status); + DOM.btnStart.disabled = hasLiveRun || isStopping; + DOM.btnStop.disabled = !canStop; + DOM.progressFill.className = isActive + ? 'progress-fill running' + : (isStopping ? 'progress-fill stopping' : 'progress-fill'); + + if (DOM.statSuccess) DOM.statSuccess.textContent = state.stats.success; + if (DOM.statFail) DOM.statFail.textContent = state.stats.fail; + if (DOM.statTotal) DOM.statTotal.textContent = state.stats.total; + + if (status === 'idle' && state.ui.countdownTimer) { + clearInterval(state.ui.countdownTimer); + state.ui.countdownTimer = null; + } +} + +function formatTaskStatusLabel(status) { + return STATUS_LABEL_MAP[status] || (status ? String(status) : '等待开始'); +} + +function getStepDisplayLabel(stepId) { + return STEP_DISPLAY_LABELS[stepId] || (stepId ? String(stepId) : '等待开始'); +} + +function getWorkerStatusLabel(status) { + return STATUS_LABEL_MAP[status] || (status ? String(status) : '等待开始'); +} + +function getPhaseLabel(phase) { + return PHASE_LABEL_MAP[phase] || (phase ? String(phase) : '等待任务'); +} + +function getCompletionSemanticsLabel(value) { + return COMPLETION_SEMANTICS_MAP[value] || '注册完成即结束'; +} + +function getWorkerSortKey(worker) { + return [ + WORKER_STATUS_PRIORITY[worker?.status] || 0, + worker?.updated_at || '', + Number(worker?.worker_id || 0), + ]; +} + +function compareWorkerRuntime(a, b) { + const [sa, ua, wa] = getWorkerSortKey(a); + const [sb, ub, wb] = getWorkerSortKey(b); + if (sa !== sb) return sb - sa; + if (ua !== ub) return ub.localeCompare(ua); + return wb - wa; +} + +function sortWorkers(workers) { + return [...workers].sort(compareWorkerRuntime); +} + +function ensureFocusWorker() { + const workers = sortWorkers(state.runtime.workers || []); + const lockedId = normalizeWorkerId(state.ui.focusWorkerId); + if (state.ui.focusLocked && lockedId && workers.some(worker => worker.worker_id === lockedId)) return; + + const backendFocus = normalizeWorkerId(state.runtime.focus_worker_id); + if (backendFocus && workers.some(worker => worker.worker_id === backendFocus)) { + state.ui.focusWorkerId = backendFocus; + return; + } + + state.ui.focusWorkerId = workers[0]?.worker_id || null; +} + +function getFocusWorker() { + const focusId = normalizeWorkerId(state.ui.focusWorkerId); + if (!focusId) return null; + return (state.runtime.workers || []).find(worker => worker.worker_id === focusId) || null; +} + +function selectFocusWorker(nextId, { lock = false } = {}) { + const normalizedId = normalizeWorkerId(nextId); + if (!normalizedId) return; + if (!(state.runtime.workers || []).some(worker => worker.worker_id === normalizedId)) return; + state.ui.focusWorkerId = normalizedId; + if (lock) state.ui.focusLocked = true; + renderRuntimePanels(); +} + +function unlockFocusWorker() { + state.ui.focusLocked = false; + ensureFocusWorker(); + renderRuntimePanels(); +} + +function mergeWorkerIntoRuntime(workerPatch) { + const normalizedWorker = normalizeWorker(workerPatch); + if (!normalizedWorker) return; + + const workers = [...(state.runtime.workers || [])]; + const index = workers.findIndex(worker => worker.worker_id === normalizedWorker.worker_id); + if (index >= 0) { + const prevWorker = workers[index]; + workers[index] = normalizeWorker({ + ...prevWorker, + ...normalizedWorker, + steps: normalizedWorker.steps.length ? normalizedWorker.steps : prevWorker.steps, + }, normalizedWorker.worker_id); + } else { + workers.push(normalizedWorker); + } + + state.runtime = { + ...state.runtime, + run_id: normalizeRunId(normalizedWorker.run_id) || state.runtime.run_id || state.task.run_id, + focus_worker_id: normalizeWorkerId(state.runtime.focus_worker_id) || normalizedWorker.worker_id, + workers: sortWorkers(workers), + }; + + ensureFocusWorker(); + renderRuntimePanels(); +} + +function upsertWorkerStep(existingSteps, stepPatch) { + const steps = normalizeWorkerSteps(existingSteps); + const normalizedStep = normalizeWorkerStep(stepPatch, steps.length); + if (!normalizedStep) return steps; + + const next = [...steps]; + const index = next.findIndex(step => step.id === normalizedStep.id); + if (index >= 0) next[index] = { ...next[index], ...normalizedStep }; + else next.push(normalizedStep); + return normalizeWorkerSteps(next); +} + +function mergeWorkerStepUpdate(event) { + const workerSource = event.worker && typeof event.worker === 'object' ? event.worker : event; + const workerId = normalizeWorkerId(workerSource.worker_id || event.worker_id); + if (!workerId) { + requestStatusSnapshot(); + return; + } + + const workers = [...(state.runtime.workers || [])]; + const index = workers.findIndex(worker => worker.worker_id === workerId); + const baseWorker = index >= 0 ? workers[index] : normalizeWorker({ worker_id: workerId, worker_label: `W${workerId}` }, workerId); + const nextWorker = normalizeWorker({ + ...baseWorker, + ...workerSource, + worker_id: workerId, + steps: workerSource.steps || upsertWorkerStep(baseWorker?.steps || [], event.step || workerSource.step || workerSource), + }, workerId); + + if (index >= 0) workers[index] = nextWorker; + else workers.push(nextWorker); + + state.runtime = { + ...state.runtime, + workers: sortWorkers(workers), + focus_worker_id: normalizeWorkerId(event.focus_worker_id) || state.runtime.focus_worker_id || workerId, + }; + + ensureFocusWorker(); + renderRuntimePanels(); +} + +function getWorkerPrimaryStep(worker) { + if (!worker) return null; + const steps = Array.isArray(worker.steps) ? worker.steps : []; + const activeStep = steps.find(step => step.status === 'active'); + if (activeStep) return activeStep; + return steps[steps.length - 1] || null; +} + +function renderTaskOverview(task, runtime, stats) { + if (!DOM.taskOverview) return; + const workers = Array.isArray(runtime?.workers) ? runtime.workers : []; + const activeWorkers = workers.filter(worker => !['idle', 'stopped'].includes(String(worker.status || 'idle'))).length; + const cards = [ + { label: '任务状态', value: formatTaskStatusLabel(task?.status || 'idle'), hint: task?.status || 'idle', status: `task-status-${task?.status || 'idle'}` }, + { label: '运行标识', value: task?.run_id || '--', hint: `revision ${normalizeRevision(task?.revision, 0)}`, status: 'task-status-meta' }, + { label: 'Worker', value: `${activeWorkers}/${workers.length}`, hint: `focus ${runtime?.focus_worker_id || '--'}`, status: 'task-status-meta' }, + { label: '成功 / 失败', value: `${stats?.success || 0} / ${stats?.fail || 0}`, hint: `total ${stats?.total || 0}`, status: 'task-status-meta' }, + ]; + + if (!task?.run_id && (task?.status || 'idle') === 'idle' && workers.length === 0) { + DOM.taskOverview.innerHTML = '
等待任务启动
'; + return; + } + + DOM.taskOverview.innerHTML = cards.map(card => ` +
+ ${escapeHtml(card.label)} + ${escapeHtml(card.value)} + ${escapeHtml(card.hint)} +
+ `).join(''); +} + +function renderWorkerList(workers, focusWorkerId) { + if (!DOM.workerList) return; + const entries = sortWorkers(Array.isArray(workers) ? workers : []); + if (!entries.length) { + DOM.workerList.innerHTML = '
暂无 Worker 运行
'; + return; + } + + DOM.workerList.innerHTML = entries.map((worker) => { + const workerId = worker.worker_id; + const focused = normalizeWorkerId(focusWorkerId) === workerId; + const primaryStep = getWorkerPrimaryStep(worker); + const stepLabel = primaryStep ? primaryStep.label : '等待开始'; + const email = worker.email || '等待邮箱创建'; + const updatedAt = worker.updated_at || '--'; + const status = String(worker.status || 'idle'); + return ` + + `; + }).join(''); + + DOM.workerList.querySelectorAll('[data-worker-id]').forEach((button) => { + button.addEventListener('click', () => selectFocusWorker(button.dataset.workerId, { lock: true })); + }); +} + +function renderWorkerDetail(focusWorker) { + if (!DOM.workerDetail) return; + if (!focusWorker) { + DOM.workerDetail.className = 'worker-detail-card empty'; + DOM.workerDetail.innerHTML = '等待任务启动'; + if (DOM.unlockFocusBtn) DOM.unlockFocusBtn.disabled = !state.ui.focusLocked; + return; + } + + const status = String(focusWorker.status || 'idle'); + const completionSemantics = getCompletionSemanticsLabel(state.runtime.completion_semantics || 'registration_only'); + const metaItems = [ + { label: 'Worker', value: focusWorker.worker_label || `W${focusWorker.worker_id}` }, + { label: '状态', value: `${getWorkerStatusLabel(status)} · ${getPhaseLabel(focusWorker.phase || 'idle')}` }, + { label: '邮箱', value: focusWorker.email || '等待邮箱创建' }, + { label: '邮箱提供商', value: focusWorker.mail_provider || '--' }, + { label: '当前步骤', value: getWorkerPrimaryStep(focusWorker)?.label || '等待开始' }, + { label: '完成语义', value: completionSemantics }, + { label: '更新时间', value: focusWorker.updated_at || '--' }, + { label: '进度消息', value: focusWorker.message || '等待后端步骤更新', wide: true }, + ]; + + const steps = Array.isArray(focusWorker.steps) ? focusWorker.steps : []; + const stepsHtml = steps.length + ? steps.map((step) => ` +
+
+ ${escapeHtml(step.label || step.step_id || step.id || '未命名步骤')} + ${escapeHtml(step.status || 'pending')} +
+ ${step.message ? `
${escapeHtml(step.message)}
` : ''} + ${step.updated_at ? `
${escapeHtml(step.updated_at)}
` : ''} +
+ `).join('') + : '
暂无步骤轨道
'; + + DOM.workerDetail.className = `worker-detail-card worker-detail-${escapeHtml(status)}`; + DOM.workerDetail.innerHTML = ` +
+ ${metaItems.map((item) => ` +
+ ${escapeHtml(item.label)} + ${escapeHtml(item.value)} +
+ `).join('')} +
+
+
步骤轨道
+
${stepsHtml}
+
+ `; + + if (DOM.unlockFocusBtn) DOM.unlockFocusBtn.disabled = !state.ui.focusLocked; +} + +function renderRuntimePanels() { + renderTaskOverview(state.task, state.runtime, state.stats); + renderWorkerList(state.runtime.workers, state.ui.focusWorkerId); + renderWorkerDetail(getFocusWorker()); +} + +function startCountdown(seconds) { + if (state.ui.countdownTimer) clearInterval(state.ui.countdownTimer); + let remaining = seconds; + const entries = DOM.logBody.querySelectorAll('.log-entry'); + const countdownEntry = entries.length > 0 ? entries[entries.length - 1] : null; + const countdownMsgEl = countdownEntry ? countdownEntry.querySelector('.log-msg') : null; + state.ui.countdownTimer = setInterval(() => { + remaining--; + if (remaining <= 0) { clearInterval(state.ui.countdownTimer); state.ui.countdownTimer = null; return; } + if (countdownMsgEl) countdownMsgEl.textContent = `休息中... 剩余 ${remaining} 秒`; + }, 1000); +} + +// ========================================== +// Token 列表 +// ========================================== +function debouncedLoadTokens() { + if (state.ui._loadTokensTimer) clearTimeout(state.ui._loadTokensTimer); + state.ui._loadTokensTimer = setTimeout(() => { + loadTokens(); + state.ui._loadTokensTimer = null; + }, 1000); +} + +async function loadTokens() { + try { + const res = await fetch('/api/tokens'); + const data = await res.json(); + state.ui.tokens = data.tokens || []; + renderTokenList(); + } catch { } +} + +function getFilteredTokens(tokens) { + const status = state.ui.tokenFilter.status || 'all'; + const keyword = (state.ui.tokenFilter.keyword || '').trim().toLowerCase(); + + return (tokens || []).filter((t) => { + const platforms = getTokenUploadedPlatforms(t); + const uploaded = platforms.length > 0; + if (status === 'synced' && !uploaded) return false; + if (status === 'unsynced' && uploaded) return false; + if (status === 'cpa' && !platforms.includes('cpa')) return false; + if (status === 'sub2api' && !platforms.includes('sub2api')) return false; + if (status === 'both' && !(platforms.includes('cpa') && platforms.includes('sub2api'))) return false; + + if (!keyword) return true; + const email = String(t.email || '').toLowerCase(); + const fname = String(t.filename || '').toLowerCase(); + return email.includes(keyword) || fname.includes(keyword); + }); +} + +function getTokenUploadedPlatforms(token) { + const platforms = new Set(); + const fromTop = Array.isArray(token && token.uploaded_platforms) ? token.uploaded_platforms : []; + const content = (token && token.content) || {}; + const fromContent = Array.isArray(content.uploaded_platforms) ? content.uploaded_platforms : []; + [...fromTop, ...fromContent].forEach((p) => { + const name = String(p || '').toLowerCase().trim(); + if (name === 'cpa' || name === 'sub2api') platforms.add(name); + }); + if (content.cpa_uploaded || content.cpa_synced) platforms.add('cpa'); + if (content.sub2api_uploaded || content.sub2api_synced || content.synced) platforms.add('sub2api'); + return ['cpa', 'sub2api'].filter((p) => platforms.has(p)); +} + +function renderTokenList() { + const allTokens = state.ui.tokens || []; + const filteredTokens = getFilteredTokens(allTokens); + updateHeaderLocalTokens(allTokens); + + if (!DOM.poolTokenList) return; + if (filteredTokens.length === 0) { + const msg = allTokens.length === 0 ? '暂无 Token' : '暂无符合筛选条件的 Token'; + DOM.poolTokenList.innerHTML = `
🔑
${msg}
`; + return; + } + DOM.poolTokenList.innerHTML = filteredTokens.map(t => renderTokenItem(t)).join(''); +} + +function applyTokenFilter() { + state.ui.tokenFilter.status = DOM.tokenFilterStatus ? DOM.tokenFilterStatus.value : 'all'; + state.ui.tokenFilter.keyword = DOM.tokenFilterKeyword ? DOM.tokenFilterKeyword.value.trim() : ''; + renderTokenList(); +} + +function resetTokenFilter() { + state.ui.tokenFilter.status = 'all'; + state.ui.tokenFilter.keyword = ''; + if (DOM.tokenFilterStatus) DOM.tokenFilterStatus.value = 'all'; + if (DOM.tokenFilterKeyword) DOM.tokenFilterKeyword.value = ''; + renderTokenList(); +} + +function renderTokenItem(t) { + const platforms = getTokenUploadedPlatforms(t); + const uploaded = platforms.length > 0; + const platformBadges = platforms.length > 0 + ? platforms.map((p) => `${p === 'cpa' ? 'CPA' : 'Sub2Api'}`).join('') + : '未上传'; + const expiredStr = formatTime(t.expired); + const tokenPayload = encodeURIComponent(JSON.stringify(t.content || {})); + const filePayload = encodeURIComponent(t.filename || ''); + return ` +
+
+
+ ${escapeHtml(t.email || t.filename)} +
+
${platformBadges}
+
过期: ${expiredStr}
+
+
+ + +
+
`; +} + +function formatTime(timeStr) { + if (!timeStr) return '未知'; + try { + const d = new Date(timeStr); + if (isNaN(d.getTime())) return timeStr; + const pad = n => String(n).padStart(2, '0'); + return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`; + } catch { return timeStr; } +} + +async function copyToken(jsonStr) { + const ok = await copyText(jsonStr); + showToast(ok ? 'Token 已复制到剪贴板' : '复制失败', ok ? 'success' : 'error'); +} + +async function copyText(text) { + if (navigator.clipboard && navigator.clipboard.writeText) { + try { await navigator.clipboard.writeText(text); return true; } catch { } + } + try { + const ta = document.createElement('textarea'); + ta.value = text; + ta.style.cssText = 'position:fixed;top:-9999px;left:-9999px;opacity:0;'; + document.body.appendChild(ta); + ta.focus(); ta.select(); + const ok = document.execCommand('copy'); + document.body.removeChild(ta); + return ok; + } catch { return false; } +} + +async function copyAllRt() { + try { + const visibleTokens = getFilteredTokens(state.ui.tokens || []); + const rts = visibleTokens.map(t => (t.content || {}).refresh_token || '').filter(Boolean); + if (rts.length === 0) { showToast('没有可用的 Refresh Token', 'error'); return; } + const ok = await copyText(rts.join('\n')); + showToast(ok ? `已复制 ${rts.length} 个 RT(当前筛选)` : '复制失败', ok ? 'success' : 'error'); + } catch (e) { showToast('复制失败: ' + e.message, 'error'); } +} + +function downloadBlob(filename, blob) { + const link = document.createElement('a'); + const url = URL.createObjectURL(blob); + link.href = url; + link.download = filename; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + URL.revokeObjectURL(url); +} + +function exportLocalTokens() { + try { + const visibleTokens = getFilteredTokens(state.ui.tokens || []); + if (visibleTokens.length === 0) { + showToast('没有可导出的 Token(当前筛选)', 'error'); + return; + } + + const exportPayload = { + exported_at: new Date().toISOString(), + total: visibleTokens.length, + filter: { + status: state.ui.tokenFilter.status || 'all', + keyword: state.ui.tokenFilter.keyword || '', + }, + tokens: visibleTokens.map((t) => ({ + filename: t.filename || '', + email: t.email || '', + uploaded_platforms: getTokenUploadedPlatforms(t), + content: t.content || {}, + })), + }; + + const status = String(state.ui.tokenFilter.status || 'all').replace(/[^a-z0-9_-]/gi, '_'); + const stamp = new Date().toISOString().replace(/[:.]/g, '-'); + const filename = `local_tokens_${status}_${stamp}.json`; + const blob = new Blob([JSON.stringify(exportPayload, null, 2)], { + type: 'application/json;charset=utf-8', + }); + downloadBlob(filename, blob); + showToast(`已导出 ${visibleTokens.length} 条 Token(当前筛选)`, 'success'); + } catch (e) { + showToast('导出失败: ' + e.message, 'error'); + } +} + +async function deleteToken(filename) { + if (!confirm(`确认删除 ${filename}?`)) return; + try { + const res = await fetch(`/api/tokens/${encodeURIComponent(filename)}`, { method: 'DELETE' }); + if (res.ok) { showToast('已删除', 'info'); loadTokens(); } + else showToast('删除失败', 'error'); + } catch { showToast('删除请求失败', 'error'); } +} + +function isSub2ApiAbnormalStatus(status) { + return SUB2API_ABNORMAL_STATUSES.has(String(status || '').trim().toLowerCase()); +} + +function getSub2ApiMaintainActionsFromForm() { + return { + refresh_abnormal_accounts: DOM.sub2apiMaintainRefreshAbnormal ? DOM.sub2apiMaintainRefreshAbnormal.checked : true, + delete_abnormal_accounts: DOM.sub2apiMaintainDeleteAbnormal ? DOM.sub2apiMaintainDeleteAbnormal.checked : true, + dedupe_duplicate_accounts: DOM.sub2apiMaintainDedupe ? DOM.sub2apiMaintainDedupe.checked : true, + }; +} + +function describeSub2ApiMaintainActions(actions = getSub2ApiMaintainActionsFromForm()) { + const labels = []; + if (actions.refresh_abnormal_accounts) labels.push('异常测活'); + if (actions.delete_abnormal_accounts) labels.push('异常清理'); + if (actions.dedupe_duplicate_accounts) labels.push('重复清理'); + return labels.length ? labels.join('、') : '无动作'; +} + +function getFilteredSub2ApiAccounts(accounts = state.ui.sub2apiAccounts || []) { + return Array.isArray(accounts) ? accounts : []; +} + +function applySub2ApiAccountFilter() { + state.ui.sub2apiAccountFilter.status = DOM.sub2apiAccountStatusFilter ? DOM.sub2apiAccountStatusFilter.value : 'all'; + state.ui.sub2apiAccountFilter.keyword = DOM.sub2apiAccountKeyword ? DOM.sub2apiAccountKeyword.value.trim() : ''; + state.ui.sub2apiAccountPager.page = 1; + loadSub2ApiAccounts(); +} + +function resetSub2ApiAccountFilter() { + state.ui.sub2apiAccountFilter.status = 'all'; + state.ui.sub2apiAccountFilter.keyword = ''; + if (DOM.sub2apiAccountStatusFilter) DOM.sub2apiAccountStatusFilter.value = 'all'; + if (DOM.sub2apiAccountKeyword) DOM.sub2apiAccountKeyword.value = ''; + state.ui.sub2apiAccountPager.page = 1; + loadSub2ApiAccounts(); +} + +async function loadSub2ApiAccounts({ silent = false } = {}) { + if (!DOM.sub2apiAccountList || state.ui.sub2apiAccountsLoading) return; + state.ui.sub2apiAccountsLoading = true; + if (!silent && DOM.sub2apiAccountActionStatus && !state.ui.sub2apiAccountActionBusy) { + DOM.sub2apiAccountActionStatus.textContent = '正在加载 Sub2Api 账号列表...'; + } + try { + const params = new URLSearchParams({ + page: String(state.ui.sub2apiAccountPager.page || 1), + page_size: String(state.ui.sub2apiAccountPager.pageSize || 20), + status: String(state.ui.sub2apiAccountFilter.status || 'all'), + keyword: String(state.ui.sub2apiAccountFilter.keyword || ''), + }); + const res = await fetch(`/api/sub2api/accounts?${params.toString()}`); + const data = await res.json(); + if (!res.ok) throw new Error(data.detail || 'Sub2Api 账号列表加载失败'); + + if (!data.configured) { + state.ui.sub2apiAccounts = []; + state.ui.selectedSub2ApiAccountIds.clear(); + state.ui.sub2apiAccountPager.total = 0; + state.ui.sub2apiAccountPager.filteredTotal = 0; + state.ui.sub2apiAccountPager.totalPages = 1; + state.ui.sub2apiAccountPager.page = 1; + renderSub2ApiAccountList('请先完成 Sub2Api 平台配置'); + if (DOM.sub2apiAccountActionStatus && !state.ui.sub2apiAccountActionBusy) { + DOM.sub2apiAccountActionStatus.textContent = data.error || 'Sub2Api 未配置'; + } + return; + } + + state.ui.sub2apiAccounts = Array.isArray(data.items) ? data.items : []; + state.ui.sub2apiAccountPager.page = parseInt(data.page, 10) || 1; + state.ui.sub2apiAccountPager.pageSize = parseInt(data.page_size, 10) || state.ui.sub2apiAccountPager.pageSize || 20; + state.ui.sub2apiAccountPager.total = parseInt(data.total, 10) || 0; + state.ui.sub2apiAccountPager.filteredTotal = parseInt(data.filtered_total, 10) || 0; + state.ui.sub2apiAccountPager.totalPages = parseInt(data.total_pages, 10) || 1; + renderSub2ApiAccountList(); + if (!silent && DOM.sub2apiAccountActionStatus && !state.ui.sub2apiAccountActionBusy) { + DOM.sub2apiAccountActionStatus.textContent = `已加载第 ${state.ui.sub2apiAccountPager.page}/${state.ui.sub2apiAccountPager.totalPages} 页,共 ${state.ui.sub2apiAccountPager.filteredTotal} 个账号`; + } + } catch (e) { + state.ui.sub2apiAccounts = []; + state.ui.sub2apiAccountPager.filteredTotal = 0; + state.ui.sub2apiAccountPager.totalPages = 1; + renderSub2ApiAccountList('Sub2Api 账号列表加载失败'); + if (DOM.sub2apiAccountActionStatus && !state.ui.sub2apiAccountActionBusy) { + DOM.sub2apiAccountActionStatus.textContent = '账号列表加载失败: ' + e.message; + } + } finally { + state.ui.sub2apiAccountsLoading = false; + refreshSub2ApiSelectionState(); + } +} + +function updateSub2ApiPagerUI() { + const pager = state.ui.sub2apiAccountPager || {}; + const page = pager.page || 1; + const totalPages = pager.totalPages || 1; + const pageSize = pager.pageSize || 20; + if (DOM.sub2apiAccountPageInfo) { + DOM.sub2apiAccountPageInfo.textContent = `第 ${page}/${totalPages} 页 · 每页 ${pageSize} 条`; + } + if (DOM.sub2apiAccountPageSize && String(DOM.sub2apiAccountPageSize.value) !== String(pageSize)) { + DOM.sub2apiAccountPageSize.value = String(pageSize); + } + if (DOM.sub2apiAccountPrevBtn) DOM.sub2apiAccountPrevBtn.disabled = state.ui.sub2apiAccountActionBusy || page <= 1; + if (DOM.sub2apiAccountNextBtn) DOM.sub2apiAccountNextBtn.disabled = state.ui.sub2apiAccountActionBusy || page >= totalPages; +} + +function changeSub2ApiAccountPage(delta) { + const nextPage = (state.ui.sub2apiAccountPager.page || 1) + delta; + const totalPages = state.ui.sub2apiAccountPager.totalPages || 1; + if (nextPage < 1 || nextPage > totalPages) return; + state.ui.sub2apiAccountPager.page = nextPage; + loadSub2ApiAccounts(); +} + +function changeSub2ApiAccountPageSize() { + const nextPageSize = DOM.sub2apiAccountPageSize ? parseInt(DOM.sub2apiAccountPageSize.value, 10) || 20 : 20; + state.ui.sub2apiAccountPager.pageSize = nextPageSize; + state.ui.sub2apiAccountPager.page = 1; + loadSub2ApiAccounts(); +} + +function renderSub2ApiAccountList(emptyMessage = '') { + const pageAccounts = getFilteredSub2ApiAccounts(state.ui.sub2apiAccounts || []); + const pager = state.ui.sub2apiAccountPager || {}; + if (!DOM.sub2apiAccountList) return; + if (pageAccounts.length === 0) { + const hasAny = (pager.filteredTotal || 0) > 0 || (pager.total || 0) > 0; + const msg = emptyMessage || (!hasAny ? '暂无 Sub2Api 账号' : '暂无符合筛选条件的账号'); + DOM.sub2apiAccountList.innerHTML = `
${escapeHtml(msg)}
`; + updateSub2ApiPagerUI(); + refreshSub2ApiSelectionState(); + return; + } + DOM.sub2apiAccountList.innerHTML = pageAccounts.map(account => renderSub2ApiAccountItem(account)).join(''); + updateSub2ApiPagerUI(); + refreshSub2ApiSelectionState(); +} + +function renderSub2ApiAccountItem(account) { + const accountId = Number(account.id || 0); + const email = account.email || account.name || `账号 ${accountId}`; + const status = String(account.status || 'unknown').trim().toLowerCase(); + const isAbnormal = isSub2ApiAbnormalStatus(status); + const selected = state.ui.selectedSub2ApiAccountIds.has(accountId); + const statusLabel = { + error: '异常', + disabled: '禁用', + normal: '正常', + active: '正常', + ok: '正常', + unknown: '未知', + }[status] || status || '未知'; + const statusClass = status === 'disabled' ? 'warn' : (isAbnormal ? 'danger' : 'ok'); + const duplicateBadges = []; + if (account.is_duplicate) { + duplicateBadges.push(`重复 ${account.duplicate_group_size || 0}`); + if (account.duplicate_keep) duplicateBadges.push('保留'); + if (account.duplicate_delete_candidate) duplicateBadges.push('候删'); + } + return ` +
+ +
+
+ ${escapeHtml(email)} + + ${duplicateBadges.join('')} +
+
ID: ${accountId} · 更新时间: ${escapeHtml(formatTime(account.updated_at))}
+
+
+ + +
+
`; +} + +function updateHeaderLocalTokens(tokens = state.ui.tokens || []) { + const allTokens = Array.isArray(tokens) ? tokens : []; + const total = allTokens.length; + const now = Date.now(); + const validCount = allTokens.filter((token) => { + const timeStr = token && token.expired; + if (!timeStr) return true; + const timestamp = new Date(timeStr).getTime(); + return !Number.isNaN(timestamp) ? timestamp > now : true; + }).length; + const fillPct = total > 0 ? Math.round((validCount / total) * 100) : 0; + const stateName = total === 0 ? 'idle' : (fillPct >= 85 ? 'ok' : fillPct >= 50 ? 'warn' : 'danger'); + + if (DOM.headerLocalTokenLabel) DOM.headerLocalTokenLabel.textContent = `${validCount} / ${total}`; + if (DOM.headerLocalTokenDelta) DOM.headerLocalTokenDelta.textContent = `${fillPct}%`; + if (DOM.headerLocalTokenBar) { + DOM.headerLocalTokenBar.style.width = `${Math.min(100, Math.max(fillPct, 0))}%`; + DOM.headerLocalTokenBar.className = `pool-chip-fill ${stateName === 'idle' ? '' : stateName}`.trim(); + } + setHeaderChipStatus(DOM.headerLocalTokenChip, stateName); + if (DOM.headerLocalTokenDelta) { + DOM.headerLocalTokenDelta.className = `pool-chip-delta ${stateName === 'idle' ? '' : stateName}`.trim(); + } +} + +function refreshSub2ApiSelectionState() { + const visibleAccounts = state.ui.sub2apiAccounts || []; + const visibleIds = visibleAccounts + .map(item => item.id) + .filter(id => Number.isInteger(id) && id > 0); + const selectedVisible = visibleIds.filter(id => state.ui.selectedSub2ApiAccountIds.has(id)).length; + const selectedTotal = Array.from(state.ui.selectedSub2ApiAccountIds).length; + + if (DOM.sub2apiAccountSelection) { + DOM.sub2apiAccountSelection.textContent = `已选 ${selectedTotal} 个,当前页 ${visibleIds.length} 个`; + } + if (DOM.sub2apiAccountSelectAll) { + const allSelected = visibleIds.length > 0 && selectedVisible === visibleIds.length; + DOM.sub2apiAccountSelectAll.checked = allSelected; + DOM.sub2apiAccountSelectAll.indeterminate = selectedVisible > 0 && selectedVisible < visibleIds.length; + } +} + +function toggleSelectAllSub2ApiAccounts() { + const visibleAccounts = state.ui.sub2apiAccounts || []; + const shouldSelect = !!(DOM.sub2apiAccountSelectAll && DOM.sub2apiAccountSelectAll.checked); + visibleAccounts.forEach((account) => { + const accountId = Number(account.id || 0); + if (!Number.isInteger(accountId) || accountId <= 0) return; + if (shouldSelect) state.ui.selectedSub2ApiAccountIds.add(accountId); + else state.ui.selectedSub2ApiAccountIds.delete(accountId); + }); + renderSub2ApiAccountList(); +} + +function getSelectedSub2ApiAccountIds() { + return Array.from(state.ui.selectedSub2ApiAccountIds) + .filter(id => Number.isInteger(id) && id > 0) + .sort((a, b) => a - b); +} + +function setSub2ApiAccountBusy(busy) { + state.ui.sub2apiAccountActionBusy = busy; + [ + DOM.sub2apiAccountApplyBtn, + DOM.sub2apiAccountResetBtn, + DOM.sub2apiAccountProbeBtn, + DOM.sub2apiAccountExceptionBtn, + DOM.sub2apiDuplicateScanBtn, + DOM.sub2apiDuplicateCleanBtn, + DOM.sub2apiAccountDeleteBtn, + DOM.sub2apiAccountPrevBtn, + DOM.sub2apiAccountNextBtn, + ].forEach((btn) => { + if (btn) btn.disabled = busy; + }); + if (DOM.sub2apiAccountSelectAll) DOM.sub2apiAccountSelectAll.disabled = busy; + if (DOM.sub2apiAccountPageSize) DOM.sub2apiAccountPageSize.disabled = busy; + if (!busy) updateSub2ApiPagerUI(); +} + +async function runSub2ApiAccountProbe(accountIds, label = '选中账号') { + if (state.ui.sub2apiAccountActionBusy) return; + const ids = (accountIds || []).filter(id => Number.isInteger(id) && id > 0); + if (!ids.length) { + showToast('请先选择至少一个账号', 'error'); + return; + } + + setSub2ApiAccountBusy(true); + if (DOM.sub2apiAccountActionStatus) DOM.sub2apiAccountActionStatus.textContent = `正在测活 ${ids.length} 个账号...`; + try { + const res = await fetch('/api/sub2api/accounts/probe', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ account_ids: ids, timeout: 30 }), + }); + const data = await res.json(); + if (!res.ok) throw new Error(data.detail || '账号测活失败'); + const msg = `${label}: 刷新成功 ${data.refreshed_ok || 0}, 恢复 ${data.recovered || 0}, 仍异常 ${data.still_abnormal || 0}`; + if (DOM.sub2apiAccountActionStatus) DOM.sub2apiAccountActionStatus.textContent = msg; + showToast(msg, 'success'); + await loadSub2ApiAccounts({ silent: true }); + pollSub2ApiPoolStatus(); + } catch (e) { + const msg = '账号测活失败: ' + e.message; + if (DOM.sub2apiAccountActionStatus) DOM.sub2apiAccountActionStatus.textContent = msg; + showToast(msg, 'error'); + } finally { + setSub2ApiAccountBusy(false); + } +} + +async function triggerSelectedSub2ApiProbe() { + await runSub2ApiAccountProbe(getSelectedSub2ApiAccountIds()); +} + +async function runSub2ApiExceptionHandling(accountIds = []) { + if (state.ui.sub2apiAccountActionBusy) return; + const ids = (accountIds || []).filter(id => Number.isInteger(id) && id > 0); + + setSub2ApiAccountBusy(true); + if (DOM.sub2apiAccountActionStatus) { + DOM.sub2apiAccountActionStatus.textContent = ids.length + ? `正在处理 ${ids.length} 个异常账号...` + : '正在处理整池异常账号...'; + } + try { + const res = await fetch('/api/sub2api/accounts/handle-exception', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ account_ids: ids, timeout: 30, delete_unresolved: true }), + }); + const data = await res.json(); + if (!res.ok) throw new Error(data.detail || '异常账号处理失败'); + const msg = `异常处理完成: 目标 ${data.targeted || 0}, 恢复 ${data.recovered || 0}, 删除 ${data.deleted_ok || 0}, 失败 ${data.deleted_fail || 0}`; + if (DOM.sub2apiAccountActionStatus) DOM.sub2apiAccountActionStatus.textContent = msg; + showToast(msg, 'success'); + await loadSub2ApiAccounts({ silent: true }); + pollSub2ApiPoolStatus(); + } catch (e) { + const msg = '异常账号处理失败: ' + e.message; + if (DOM.sub2apiAccountActionStatus) DOM.sub2apiAccountActionStatus.textContent = msg; + showToast(msg, 'error'); + } finally { + setSub2ApiAccountBusy(false); + } +} + +async function triggerSub2ApiExceptionHandling() { + const ids = getSelectedSub2ApiAccountIds(); + if (ids.length) { + if (!confirm(`确认处理 ${ids.length} 个已选账号?系统会先测活,仍异常的账号会被删除。`)) return; + await runSub2ApiExceptionHandling(ids); + return; + } + if (!confirm('未选择账号,将处理整个 Sub2Api 池中的异常账号。是否继续?')) return; + await runSub2ApiExceptionHandling([]); +} + +async function runSub2ApiAccountDelete(accountIds, label = '选中账号', requireConfirm = true) { + if (state.ui.sub2apiAccountActionBusy) return; + const ids = (accountIds || []).filter(id => Number.isInteger(id) && id > 0); + if (!ids.length) { + showToast('请先选择至少一个账号', 'error'); + return; + } + if (requireConfirm && !confirm(`确认删除 ${label}(共 ${ids.length} 个)?`)) return; + + setSub2ApiAccountBusy(true); + if (DOM.sub2apiAccountActionStatus) DOM.sub2apiAccountActionStatus.textContent = `正在删除 ${ids.length} 个账号...`; + try { + const res = await fetch('/api/sub2api/accounts/delete', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ account_ids: ids, timeout: 20 }), + }); + const data = await res.json(); + if (!res.ok) throw new Error(data.detail || '批量删除失败'); + ids.forEach(id => state.ui.selectedSub2ApiAccountIds.delete(id)); + const msg = `批量删除完成: 成功 ${data.deleted_ok || 0}, 失败 ${data.deleted_fail || 0}`; + if (DOM.sub2apiAccountActionStatus) DOM.sub2apiAccountActionStatus.textContent = msg; + showToast(msg, 'success'); + await loadSub2ApiAccounts({ silent: true }); + pollSub2ApiPoolStatus(); + } catch (e) { + const msg = '批量删除失败: ' + e.message; + if (DOM.sub2apiAccountActionStatus) DOM.sub2apiAccountActionStatus.textContent = msg; + showToast(msg, 'error'); + } finally { + setSub2ApiAccountBusy(false); + } +} + +async function triggerSelectedSub2ApiDelete() { + await runSub2ApiAccountDelete(getSelectedSub2ApiAccountIds()); +} + +async function previewSub2ApiDuplicates() { + if (state.ui.sub2apiAccountActionBusy) return; + setSub2ApiAccountBusy(true); + if (DOM.sub2apiAccountActionStatus) DOM.sub2apiAccountActionStatus.textContent = '正在检测重复账号...'; + try { + const res = await fetch('/api/sub2api/pool/dedupe', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ dry_run: true, timeout: 20 }), + }); + const data = await res.json(); + if (!res.ok) throw new Error(data.detail || '重复账号检测失败'); + const msg = `重复预检完成: 重复组 ${data.duplicate_groups || 0}, 重复账号 ${data.duplicate_accounts || 0}, 可删 ${data.to_delete || 0}`; + if (DOM.sub2apiAccountActionStatus) DOM.sub2apiAccountActionStatus.textContent = msg; + showToast(msg, 'success'); + await loadSub2ApiAccounts({ silent: true }); + } catch (e) { + const msg = '重复账号检测失败: ' + e.message; + if (DOM.sub2apiAccountActionStatus) DOM.sub2apiAccountActionStatus.textContent = msg; + showToast(msg, 'error'); + } finally { + setSub2ApiAccountBusy(false); + } +} + +async function cleanupSub2ApiDuplicates() { + if (state.ui.sub2apiAccountActionBusy) return; + if (!confirm('确认清理 Sub2Api 中的重复账号?系统会保留每组中更新时间最新的账号。')) return; + setSub2ApiAccountBusy(true); + if (DOM.sub2apiAccountActionStatus) DOM.sub2apiAccountActionStatus.textContent = '正在清理重复账号...'; + try { + const res = await fetch('/api/sub2api/pool/dedupe', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ dry_run: false, timeout: 20 }), + }); + const data = await res.json(); + if (!res.ok) throw new Error(data.detail || '重复账号清理失败'); + const msg = `重复清理完成: 删除成功 ${data.deleted_ok || 0}, 删除失败 ${data.deleted_fail || 0}`; + if (DOM.sub2apiAccountActionStatus) DOM.sub2apiAccountActionStatus.textContent = msg; + showToast(msg, 'success'); + await loadSub2ApiAccounts({ silent: true }); + pollSub2ApiPoolStatus(); + } catch (e) { + const msg = '重复账号清理失败: ' + e.message; + if (DOM.sub2apiAccountActionStatus) DOM.sub2apiAccountActionStatus.textContent = msg; + showToast(msg, 'error'); + } finally { + setSub2ApiAccountBusy(false); + } +} + +// ========================================== +// Sub2Api 同步配置 +// ========================================== +async function loadSyncConfig() { + if (DOM.syncStatus) DOM.syncStatus.textContent = ''; + try { + const res = await fetch('/api/sync-config'); + const cfg = await res.json(); + DOM.sub2apiBaseUrl.value = cfg.base_url || ''; + if (cfg.email) DOM.sub2apiEmail.value = cfg.email; + DOM.autoSyncCheck.checked = !!cfg.auto_sync; + if (DOM.uploadMode) DOM.uploadMode.value = cfg.upload_mode || 'snapshot'; + if (DOM.uploadModeStatus) DOM.uploadModeStatus.textContent = ''; + if (DOM.sub2apiMinCandidates) DOM.sub2apiMinCandidates.value = cfg.sub2api_min_candidates || 200; + if (DOM.sub2apiInterval) DOM.sub2apiInterval.value = cfg.sub2api_maintain_interval_minutes || 30; + if (DOM.sub2apiAutoMaintain) DOM.sub2apiAutoMaintain.checked = !!cfg.sub2api_auto_maintain; + const maintainActions = cfg.sub2api_maintain_actions || {}; + if (DOM.sub2apiMaintainRefreshAbnormal) { + DOM.sub2apiMaintainRefreshAbnormal.checked = maintainActions.refresh_abnormal_accounts !== false; + } + if (DOM.sub2apiMaintainDeleteAbnormal) { + DOM.sub2apiMaintainDeleteAbnormal.checked = maintainActions.delete_abnormal_accounts !== false; + } + if (DOM.sub2apiMaintainDedupe) { + DOM.sub2apiMaintainDedupe.checked = maintainActions.dedupe_duplicate_accounts !== false; + } + if (DOM.multithreadCheck) DOM.multithreadCheck.checked = !!cfg.multithread; + if (DOM.threadCountInput) DOM.threadCountInput.value = cfg.thread_count || 3; + if (cfg.proxy && DOM.proxyInput) DOM.proxyInput.value = cfg.proxy; + if (DOM.autoRegisterCheck) DOM.autoRegisterCheck.checked = !!cfg.auto_register; + if (DOM.syncStatus) DOM.syncStatus.textContent = ''; + } catch { } +} + +async function loadProxyPoolConfig() { + try { + const res = await fetch('/api/proxy-pool/config'); + const cfg = await res.json(); + if (DOM.proxyPoolEnabled) DOM.proxyPoolEnabled.checked = !!cfg.proxy_pool_enabled; + if (DOM.proxyPoolApiUrl) DOM.proxyPoolApiUrl.value = cfg.proxy_pool_api_url || 'https://zenproxy.top/api/fetch'; + if (DOM.proxyPoolAuthMode) DOM.proxyPoolAuthMode.value = cfg.proxy_pool_auth_mode || 'query'; + if (DOM.proxyPoolCount) DOM.proxyPoolCount.value = cfg.proxy_pool_count || 1; + if (DOM.proxyPoolCountry) DOM.proxyPoolCountry.value = (cfg.proxy_pool_country || 'US').toUpperCase(); + if (DOM.proxyPoolApiKey) { + DOM.proxyPoolApiKey.value = ''; + DOM.proxyPoolApiKey.placeholder = cfg.proxy_pool_api_key_preview + ? `已保存: ${cfg.proxy_pool_api_key_preview}` + : '请输入代理池 API Key'; + } + if (DOM.proxyPoolStatus) DOM.proxyPoolStatus.textContent = ''; + } catch { } +} + +async function saveProxyPoolConfig() { + if (!DOM.proxyPoolSaveBtn) return; + const payload = { + proxy_pool_enabled: DOM.proxyPoolEnabled ? DOM.proxyPoolEnabled.checked : true, + proxy_pool_api_url: DOM.proxyPoolApiUrl ? DOM.proxyPoolApiUrl.value.trim() : 'https://zenproxy.top/api/fetch', + proxy_pool_auth_mode: DOM.proxyPoolAuthMode ? DOM.proxyPoolAuthMode.value : 'query', + proxy_pool_api_key: DOM.proxyPoolApiKey ? DOM.proxyPoolApiKey.value.trim() : '', + proxy_pool_count: DOM.proxyPoolCount ? (parseInt(DOM.proxyPoolCount.value, 10) || 1) : 1, + proxy_pool_country: DOM.proxyPoolCountry ? DOM.proxyPoolCountry.value.trim().toUpperCase() : 'US', + }; + if (!payload.proxy_pool_api_url) { + showToast('请填写代理池 API 地址', 'error'); + return; + } + if (payload.proxy_pool_count < 1) payload.proxy_pool_count = 1; + if (!payload.proxy_pool_country) payload.proxy_pool_country = 'US'; + + DOM.proxyPoolSaveBtn.disabled = true; + const oldText = DOM.proxyPoolSaveBtn.textContent; + DOM.proxyPoolSaveBtn.textContent = '保存中...'; + if (DOM.proxyPoolStatus) DOM.proxyPoolStatus.textContent = '正在保存代理池配置...'; + try { + const res = await fetch('/api/proxy-pool/config', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload), + }); + const data = await res.json(); + if (!res.ok) { + const msg = data.detail || '保存失败'; + if (DOM.proxyPoolStatus) DOM.proxyPoolStatus.textContent = msg; + showToast(msg, 'error'); + return; + } + if (DOM.proxyPoolApiKey && payload.proxy_pool_api_key) { + DOM.proxyPoolApiKey.value = ''; + DOM.proxyPoolApiKey.placeholder = `已保存: ${payload.proxy_pool_api_key.slice(0, 8)}...`; + } + const msg = '代理池配置已保存'; + if (DOM.proxyPoolStatus) DOM.proxyPoolStatus.textContent = msg; + showToast(msg, 'success'); + } catch (e) { + const msg = '请求失败: ' + e.message; + if (DOM.proxyPoolStatus) DOM.proxyPoolStatus.textContent = msg; + showToast(msg, 'error'); + } finally { + DOM.proxyPoolSaveBtn.disabled = false; + DOM.proxyPoolSaveBtn.textContent = oldText || '保存代理池配置'; + } +} + +async function saveSyncConfig() { + const base_url = DOM.sub2apiBaseUrl.value.trim(); + const email = DOM.sub2apiEmail.value.trim(); + const password = DOM.sub2apiPassword.value.trim(); + const auto_sync = !!DOM.autoSyncCheck.checked; + const upload_mode = DOM.uploadMode ? DOM.uploadMode.value : 'snapshot'; + const sub2api_min_candidates = parseInt(DOM.sub2apiMinCandidates.value) || 200; + const sub2api_auto_maintain = DOM.sub2apiAutoMaintain.checked; + const sub2api_maintain_interval_minutes = parseInt(DOM.sub2apiInterval.value) || 30; + const sub2api_maintain_actions = getSub2ApiMaintainActionsFromForm(); + const multithread = DOM.multithreadCheck ? DOM.multithreadCheck.checked : false; + const thread_count = DOM.threadCountInput ? parseInt(DOM.threadCountInput.value) || 3 : 3; + const auto_register = DOM.autoRegisterCheck ? DOM.autoRegisterCheck.checked : false; + + if (!base_url) { showToast('请填写平台地址', 'error'); return; } + if (!email) { showToast('请填写邮箱', 'error'); return; } + + DOM.saveSyncConfigBtn.disabled = true; + DOM.saveSyncConfigBtn.textContent = '验证中...'; + DOM.syncStatus.textContent = '正在验证账号密码...'; + try { + const res = await fetch('/api/sync-config', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + base_url, email, password, account_name: 'AutoReg', auto_sync, + upload_mode, + sub2api_min_candidates, sub2api_auto_maintain, sub2api_maintain_interval_minutes, + sub2api_maintain_actions, + multithread, thread_count, auto_register, + }), + }); + const data = await res.json(); + if (res.ok) { + showToast('验证通过,配置已保存', 'success'); + DOM.syncStatus.textContent = '验证通过,配置已保存'; + pollSub2ApiPoolStatus(); + loadSub2ApiAccounts(); + } else { + showToast(data.detail || '验证失败', 'error'); + DOM.syncStatus.textContent = data.detail || '验证失败'; + } + } catch (e) { + showToast('请求失败: ' + e.message, 'error'); + DOM.syncStatus.textContent = '请求失败: ' + e.message; + } finally { + DOM.saveSyncConfigBtn.disabled = false; + DOM.saveSyncConfigBtn.textContent = '保存'; + } +} + +async function saveUploadMode() { + const upload_mode = DOM.uploadMode ? DOM.uploadMode.value : 'snapshot'; + if (!DOM.uploadModeSaveBtn) return; + DOM.uploadModeSaveBtn.disabled = true; + const oldText = DOM.uploadModeSaveBtn.textContent; + DOM.uploadModeSaveBtn.textContent = '保存中...'; + if (DOM.uploadModeStatus) DOM.uploadModeStatus.textContent = '正在保存策略...'; + try { + const res = await fetch('/api/upload-mode', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ upload_mode }), + }); + const data = await res.json(); + if (!res.ok) { + const msg = data.detail || '保存失败'; + showToast(msg, 'error'); + if (DOM.uploadModeStatus) DOM.uploadModeStatus.textContent = msg; + return; + } + const label = upload_mode === 'decoupled' ? '双平台同传(单账号双上传)' : '串行补平台(先CPA后Sub2Api)'; + showToast('上传策略已保存:' + label, 'success'); + if (DOM.uploadModeStatus) DOM.uploadModeStatus.textContent = '已保存:' + label; + } catch (e) { + showToast('请求失败: ' + e.message, 'error'); + if (DOM.uploadModeStatus) DOM.uploadModeStatus.textContent = '请求失败: ' + e.message; + } finally { + DOM.uploadModeSaveBtn.disabled = false; + DOM.uploadModeSaveBtn.textContent = oldText || '保存策略'; + } +} + +async function batchSync() { + const btn = DOM.poolPwSyncBtn; + if (!btn) return; + btn.disabled = true; + btn.textContent = '导入中...'; + showToast('批量导入开始', 'info'); + try { + const res = await fetch('/api/sync-batch', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ filenames: [] }), + }); + const data = await res.json(); + if (!res.ok) { showToast(data.detail || '导入失败', 'error'); return; } + const msg = `导入完成:共 ${data.total},成功 ${data.ok},失败 ${data.fail}`; + showToast(msg, data.fail > 0 ? 'info' : 'success'); + } catch (e) { + showToast('导入失败: ' + e.message, 'error'); + } finally { + btn.disabled = false; + btn.textContent = '批量导入'; + } +} + +// ========================================== +// CPA 配置 +// ========================================== +async function loadPoolConfig() { + try { + const res = await fetch('/api/pool/config'); + const cfg = await res.json(); + DOM.cpaBaseUrl.value = cfg.cpa_base_url || ''; + DOM.cpaMinCandidates.value = cfg.min_candidates || 800; + DOM.cpaUsedPercent.value = cfg.used_percent_threshold || 95; + DOM.cpaAutoMaintain.checked = !!cfg.auto_maintain; + DOM.cpaInterval.value = cfg.maintain_interval_minutes || 30; + if (DOM.cpaStatus) DOM.cpaStatus.textContent = ''; + } catch { } +} + +async function savePoolConfig() { + const payload = { + cpa_base_url: DOM.cpaBaseUrl.value.trim(), + cpa_token: DOM.cpaToken.value.trim(), + min_candidates: parseInt(DOM.cpaMinCandidates.value) || 800, + used_percent_threshold: parseInt(DOM.cpaUsedPercent.value) || 95, + auto_maintain: DOM.cpaAutoMaintain.checked, + maintain_interval_minutes: parseInt(DOM.cpaInterval.value) || 30, + }; + DOM.cpaSaveBtn.disabled = true; + try { + const res = await fetch('/api/pool/config', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload), + }); + if (res.ok) { + showToast('CPA 配置已保存', 'success'); + DOM.cpaStatus.textContent = '配置已保存'; + pollPoolStatus(); + } else { + const data = await res.json(); + showToast(data.detail || '保存失败', 'error'); + DOM.cpaStatus.textContent = data.detail || '保存失败'; + } + } catch (e) { + showToast('请求失败: ' + e.message, 'error'); + DOM.cpaStatus.textContent = '请求失败'; + } finally { + DOM.cpaSaveBtn.disabled = false; + } +} + +async function testCpaConnection() { + DOM.cpaTestBtn.disabled = true; + DOM.cpaStatus.textContent = '测试中...'; + try { + const res = await fetch('/api/pool/check', { method: 'POST' }); + const data = await res.json(); + if (data.ok) { + DOM.cpaStatus.textContent = data.message || '连接成功'; + showToast('CPA 连接成功', 'success'); + } else { + DOM.cpaStatus.textContent = data.message || data.detail || '连接失败'; + showToast('CPA 连接失败', 'error'); + } + } catch (e) { + DOM.cpaStatus.textContent = '请求失败: ' + e.message; + } finally { + DOM.cpaTestBtn.disabled = false; + } +} + +// ========================================== +// 池状态轮询 +// ========================================== +async function pollPoolStatus() { + try { + const res = await fetch('/api/pool/status'); + const data = await res.json(); + + if (!data.configured) { + if (DOM.poolTotal) DOM.poolTotal.textContent = '--'; + if (DOM.poolCandidates) DOM.poolCandidates.textContent = '--'; + if (DOM.poolError) DOM.poolError.textContent = '--'; + if (DOM.poolThreshold) DOM.poolThreshold.textContent = '--'; + if (DOM.poolPercent) DOM.poolPercent.textContent = '--'; + updateHeaderCpa(null); + return; + } + + const candidates = data.candidates || 0; + const errorCount = data.error_count || 0; + const threshold = data.threshold || 0; + const fillPct = threshold > 0 ? Math.round(candidates / threshold * 100) : 100; + + if (DOM.poolTotal) DOM.poolTotal.textContent = data.total || 0; + if (DOM.poolCandidates) DOM.poolCandidates.textContent = candidates; + if (DOM.poolError) { + DOM.poolError.textContent = errorCount; + DOM.poolError.className = `stat-value ${errorCount > 0 ? 'red' : 'green'}`; + } + if (DOM.poolThreshold) DOM.poolThreshold.textContent = threshold; + if (DOM.poolPercent) { + DOM.poolPercent.textContent = fillPct + '%'; + DOM.poolPercent.className = `stat-value ${fillPct >= 100 ? 'green' : fillPct >= 80 ? 'yellow' : 'red'}`; + } + + updateHeaderCpa({ candidates, threshold, fillPct, errorCount }); + } catch { } +} + +async function triggerMaintenance() { + DOM.poolMaintainBtn.disabled = true; + DOM.poolMaintainBtn.textContent = '维护中...'; + DOM.poolMaintainStatus.textContent = '正在探测并清理无效账号...'; + try { + const res = await fetch('/api/pool/maintain', { method: 'POST' }); + const data = await res.json(); + if (res.ok) { + const msg = `维护完成: 无效 ${data.invalid_count || 0}, 已删除 ${data.deleted_ok || 0}, 失败 ${data.deleted_fail || 0}`; + DOM.poolMaintainStatus.textContent = msg; + showToast(msg, 'success'); + pollPoolStatus(); + } else { + DOM.poolMaintainStatus.textContent = data.detail || '维护失败'; + showToast(data.detail || '维护失败', 'error'); + } + } catch (e) { + DOM.poolMaintainStatus.textContent = '请求失败: ' + e.message; + showToast('维护请求失败', 'error'); + } finally { + DOM.poolMaintainBtn.disabled = false; + DOM.poolMaintainBtn.textContent = '维护'; + } +} + +// ========================================== +// Sub2Api 池状态轮询 +// ========================================== +async function pollSub2ApiPoolStatus() { + try { + const res = await fetch('/api/sub2api/pool/status'); + const data = await res.json(); + + if (data.configured && data.error) { + if (DOM.sub2apiPoolMaintainStatus) DOM.sub2apiPoolMaintainStatus.textContent = 'Sub2Api 状态获取失败: ' + data.error; + updateHeaderSub2Api(null); + return; + } + + if (!data.configured) { + if (DOM.sub2apiPoolTotal) DOM.sub2apiPoolTotal.textContent = '--'; + if (DOM.sub2apiPoolNormal) DOM.sub2apiPoolNormal.textContent = '--'; + if (DOM.sub2apiPoolError) DOM.sub2apiPoolError.textContent = '--'; + if (DOM.sub2apiPoolThreshold) DOM.sub2apiPoolThreshold.textContent = '--'; + if (DOM.sub2apiPoolPercent) DOM.sub2apiPoolPercent.textContent = '--'; + updateHeaderSub2Api(null); + return; + } + + const normal = data.candidates || 0; + const error = data.error_count || 0; + const total = data.total || 0; + const threshold = data.threshold || 0; + // 充足率: 正常账号 / 目标阈值 + const fillPct = threshold > 0 ? Math.round(normal / threshold * 100) : 100; + // 健康率: 正常账号 / 总账号 (无异常就是 100%) + const healthPct = total > 0 ? Math.round(normal / total * 100) : 100; + + if (DOM.sub2apiPoolTotal) DOM.sub2apiPoolTotal.textContent = total; + if (DOM.sub2apiPoolNormal) DOM.sub2apiPoolNormal.textContent = normal; + if (DOM.sub2apiPoolError) { + DOM.sub2apiPoolError.textContent = error; + DOM.sub2apiPoolError.className = `stat-value ${error > 0 ? 'red' : 'green'}`; + } + if (DOM.sub2apiPoolThreshold) DOM.sub2apiPoolThreshold.textContent = threshold; + if (DOM.sub2apiPoolPercent) { + DOM.sub2apiPoolPercent.textContent = fillPct + '%'; + DOM.sub2apiPoolPercent.className = `stat-value ${fillPct >= 100 ? 'green' : fillPct >= 80 ? 'yellow' : 'red'}`; + } + + updateHeaderSub2Api({ normal, threshold, fillPct, error }); + } catch { } +} + +function updateHeaderSub2Api(data) { + if (!data) { + if (DOM.headerSub2apiLabel) DOM.headerSub2apiLabel.textContent = '-- / --'; + if (DOM.headerSub2apiDelta) DOM.headerSub2apiDelta.textContent = '--'; + if (DOM.headerSub2apiBar) DOM.headerSub2apiBar.style.width = '0%'; + setHeaderChipStatus(DOM.headerSub2apiChip, 'idle'); + if (DOM.headerSub2apiBar) DOM.headerSub2apiBar.className = 'pool-chip-fill'; + if (DOM.headerSub2apiDelta) DOM.headerSub2apiDelta.className = 'pool-chip-delta'; + return; + } + const { normal, threshold, fillPct, error: errorCount } = data; + const state = _headerPoolState(fillPct, errorCount); + if (DOM.headerSub2apiLabel) DOM.headerSub2apiLabel.textContent = `${normal} / ${threshold}`; + if (DOM.headerSub2apiDelta) DOM.headerSub2apiDelta.textContent = _headerPoolDelta(fillPct); + if (DOM.headerSub2apiBar) { + DOM.headerSub2apiBar.style.width = Math.min(100, fillPct) + '%'; + DOM.headerSub2apiBar.className = `pool-chip-fill ${state}`; + } + setHeaderChipStatus(DOM.headerSub2apiChip, state); + if (DOM.headerSub2apiDelta) DOM.headerSub2apiDelta.className = `pool-chip-delta ${state}`; +} + +function updateHeaderCpa(data) { + if (!data) { + if (DOM.headerCpaLabel) DOM.headerCpaLabel.textContent = '-- / --'; + if (DOM.headerCpaDelta) DOM.headerCpaDelta.textContent = '--'; + if (DOM.headerCpaBar) DOM.headerCpaBar.style.width = '0%'; + setHeaderChipStatus(DOM.headerCpaChip, 'idle'); + if (DOM.headerCpaBar) DOM.headerCpaBar.className = 'pool-chip-fill'; + if (DOM.headerCpaDelta) DOM.headerCpaDelta.className = 'pool-chip-delta'; + return; + } + const { candidates, threshold, fillPct, errorCount } = data; + const state = _headerPoolState(fillPct, errorCount); + if (DOM.headerCpaLabel) DOM.headerCpaLabel.textContent = `${candidates} / ${threshold}`; + if (DOM.headerCpaDelta) DOM.headerCpaDelta.textContent = _headerPoolDelta(fillPct); + if (DOM.headerCpaBar) { + DOM.headerCpaBar.style.width = Math.min(100, fillPct) + '%'; + DOM.headerCpaBar.className = `pool-chip-fill ${state}`; + } + setHeaderChipStatus(DOM.headerCpaChip, state); + if (DOM.headerCpaDelta) DOM.headerCpaDelta.className = `pool-chip-delta ${state}`; +} + +function setHeaderChipStatus(chip, state) { + if (!chip) return; + chip.classList.remove('status-idle', 'status-warn', 'status-danger', 'status-ok', 'status-over'); + chip.classList.add(`status-${state}`); +} + +function _headerPoolState(fillPct, errorCount) { + if (errorCount > 0) return 'danger'; + if (fillPct > 110) return 'over'; + if (fillPct >= 100) return 'ok'; + if (fillPct >= 80) return 'warn'; + return 'danger'; +} + +function _headerPoolDelta(fillPct) { + if (!Number.isFinite(fillPct)) return '--'; + const delta = Math.round(fillPct - 100); + if (delta === 0) return '0%'; + return `${delta > 0 ? '+' : ''}${delta}%`; +} + +async function triggerSub2ApiMaintenance() { + const actionsText = describeSub2ApiMaintainActions(); + DOM.sub2apiPoolMaintainBtn.disabled = true; + DOM.sub2apiPoolMaintainBtn.textContent = '维护中...'; + DOM.sub2apiPoolMaintainStatus.textContent = `正在维护(${actionsText})...`; + try { + const res = await fetch('/api/sub2api/pool/maintain', { method: 'POST' }); + const data = await res.json(); + if (res.ok) { + const sec = Math.max(0, Number(data.duration_ms || 0) / 1000).toFixed(2); + const msg = `维护完成(${actionsText}): 异常 ${data.error_count || 0}, 刷新恢复 ${data.refreshed || 0}, 重复组 ${data.duplicate_groups || 0}, 删除 ${data.deleted_ok || 0}, 失败 ${data.deleted_fail || 0}, ${sec}s`; + DOM.sub2apiPoolMaintainStatus.textContent = msg; + showToast(msg, 'success'); + pollSub2ApiPoolStatus(); + loadSub2ApiAccounts({ silent: true }); + } else { + DOM.sub2apiPoolMaintainStatus.textContent = data.detail || '维护失败'; + showToast(data.detail || '维护失败', 'error'); + } + } catch (e) { + DOM.sub2apiPoolMaintainStatus.textContent = '请求失败: ' + e.message; + showToast('Sub2Api 维护请求失败', 'error'); + } finally { + DOM.sub2apiPoolMaintainBtn.disabled = false; + DOM.sub2apiPoolMaintainBtn.textContent = '维护'; + } +} + +async function testProxyPoolFetch() { + if (!DOM.proxyPoolTestBtn) return; + DOM.proxyPoolTestBtn.disabled = true; + const oldText = DOM.proxyPoolTestBtn.textContent; + if (DOM.proxyPoolStatus) DOM.proxyPoolStatus.textContent = '正在测试代理池取号...'; + DOM.proxyPoolTestBtn.textContent = '测试取号中...'; + try { + const payload = { + enabled: DOM.proxyPoolEnabled ? DOM.proxyPoolEnabled.checked : true, + api_url: DOM.proxyPoolApiUrl ? DOM.proxyPoolApiUrl.value.trim() : 'https://zenproxy.top/api/fetch', + auth_mode: DOM.proxyPoolAuthMode ? DOM.proxyPoolAuthMode.value : 'query', + api_key: DOM.proxyPoolApiKey ? DOM.proxyPoolApiKey.value.trim() : '', + count: DOM.proxyPoolCount ? (parseInt(DOM.proxyPoolCount.value, 10) || 1) : 1, + country: DOM.proxyPoolCountry ? DOM.proxyPoolCountry.value.trim().toUpperCase() : 'US', + }; + const res = await fetch('/api/proxy-pool/test', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload), + }); + const data = await res.json(); + if (!res.ok || !data.ok) { + const msg = data.error || data.detail || '代理池取号失败'; + if (DOM.proxyPoolStatus) DOM.proxyPoolStatus.textContent = msg; + showToast(msg, 'error'); + return; + } + const locText = data.loc ? ` loc=${data.loc}` : ''; + const supportText = data.supported === null || data.supported === undefined + ? '' + : (data.supported ? ' 可用' : ' 不可用(CN/HK)'); + const traceWarn = data.trace_error ? `;trace失败: ${data.trace_error}` : ''; + const msg = `取号成功: ${data.proxy}${locText}${supportText}${traceWarn}`; + if (DOM.proxyPoolStatus) DOM.proxyPoolStatus.textContent = msg; + showToast('代理池取号成功', 'success'); + } catch (e) { + const msg = '测试请求失败: ' + e.message; + if (DOM.proxyPoolStatus) DOM.proxyPoolStatus.textContent = msg; + showToast(msg, 'error'); + } finally { + DOM.proxyPoolTestBtn.disabled = false; + if (DOM.syncStatus) DOM.syncStatus.textContent = ''; + DOM.proxyPoolTestBtn.textContent = oldText || '测试代理池取号'; + } +} + +async function testSub2ApiPoolConnection() { + DOM.sub2apiTestPoolBtn.disabled = true; + DOM.syncStatus.textContent = '测试连接中...'; + try { + const res = await fetch('/api/sub2api/pool/check', { method: 'POST' }); + const data = await res.json(); + if (data.ok) { + DOM.syncStatus.textContent = data.message || '连接成功'; + showToast('Sub2Api 池连接成功', 'success'); + } else { + DOM.syncStatus.textContent = data.message || data.detail || '连接失败'; + showToast('Sub2Api 池连接失败', 'error'); + } + } catch (e) { + DOM.syncStatus.textContent = '请求失败: ' + e.message; + } finally { + DOM.sub2apiTestPoolBtn.disabled = false; + } +} + +// ========================================== +// 邮箱配置(多选) +// ========================================== + +function initMailCheckboxes() { + document.querySelectorAll('.mail-provider-check').forEach(cb => { + cb.setAttribute('aria-expanded', cb.checked); + cb.addEventListener('change', () => { + const item = cb.closest('.provider-item'); + const config = item.querySelector('.provider-config'); + if (config) config.style.display = cb.checked ? 'block' : 'none'; + cb.setAttribute('aria-expanded', cb.checked); + }); + }); +} + +async function loadMailConfig() { + try { + const res = await fetch('/api/mail/config'); + const data = await res.json(); + const providers = data.mail_providers || [data.mail_provider || 'mailtm']; + const configs = data.mail_provider_configs || {}; + const strategy = data.mail_strategy || 'round_robin'; + + // 设置 checkboxes + document.querySelectorAll('.mail-provider-check').forEach(cb => { + const name = cb.value; + cb.checked = providers.includes(name); + const item = cb.closest('.provider-item'); + const configDiv = item.querySelector('.provider-config'); + if (configDiv) configDiv.style.display = cb.checked ? 'block' : 'none'; + + // 填充 per-provider 配置 + const pcfg = configs[name] || {}; + item.querySelectorAll('[data-key]').forEach(input => { + const key = input.dataset.key; + const previewKey = key + '_preview'; + if (pcfg[key]) input.value = pcfg[key]; + else if (pcfg[previewKey]) input.placeholder = pcfg[previewKey]; + }); + }); + + // 兼容旧格式 + if (!data.mail_providers && data.mail_config) { + const mc = data.mail_config; + const activeProvider = data.mail_provider || 'mailtm'; + const item = document.querySelector(`.provider-item[data-provider="${activeProvider}"]`); + if (item) { + const apiBaseInput = item.querySelector('[data-key="api_base"]'); + if (apiBaseInput && mc.api_base) apiBaseInput.value = mc.api_base; + } + } + + if (DOM.mailStrategySelect) DOM.mailStrategySelect.value = strategy; + } catch { } +} + +async function saveMailConfig() { + const checkedProviders = []; + const providerConfigs = {}; + + document.querySelectorAll('.mail-provider-check').forEach(cb => { + const name = cb.value; + if (cb.checked) { + checkedProviders.push(name); + const item = cb.closest('.provider-item'); + const cfg = {}; + item.querySelectorAll('[data-key]').forEach(input => { + cfg[input.dataset.key] = input.value.trim(); + }); + providerConfigs[name] = cfg; + } + }); + + if (checkedProviders.length === 0) { + showToast('请至少选择一个邮箱提供商', 'error'); + return false; + } + + const strategy = DOM.mailStrategySelect ? DOM.mailStrategySelect.value : 'round_robin'; + DOM.mailSaveBtn.disabled = true; + try { + const res = await fetch('/api/mail/config', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + mail_provider: checkedProviders[0], + mail_config: providerConfigs[checkedProviders[0]] || {}, + mail_providers: checkedProviders, + mail_provider_configs: providerConfigs, + mail_strategy: strategy, + }), + }); + if (res.ok) { + showToast('邮箱配置已保存', 'success'); + DOM.mailStatus.textContent = '配置已保存'; + return true; + } else { + const data = await res.json(); + DOM.mailStatus.textContent = data.detail || '保存失败'; + showToast(DOM.mailStatus.textContent, 'error'); + return false; + } + } catch (e) { + DOM.mailStatus.textContent = '请求失败: ' + e.message; + showToast(DOM.mailStatus.textContent, 'error'); + return false; + } finally { + DOM.mailSaveBtn.disabled = false; + } +} + +async function testMailConnection() { + DOM.mailTestBtn.disabled = true; + DOM.mailStatus.textContent = '测试中...'; + try { + const saved = await saveMailConfig(); + if (!saved) return; + const res = await fetch('/api/mail/test', { method: 'POST' }); + const data = await res.json(); + if (data.results) { + const msgs = data.results.map(r => `${r.provider}: ${r.ok ? 'OK' : r.message}`); + DOM.mailStatus.textContent = msgs.join(' | '); + } else { + DOM.mailStatus.textContent = data.message || (data.ok ? '连接成功' : '连接失败'); + } + showToast(data.ok ? '邮箱测试通过' : '邮箱测试失败', data.ok ? 'success' : 'error'); + } catch (e) { + DOM.mailStatus.textContent = '请求失败: ' + e.message; + } finally { + DOM.mailTestBtn.disabled = false; + } +} + +// ========================================== +// Toast 通知 — 带图标和退出动画 +// ========================================== +const TOAST_ICONS = { + success: '✓', + error: '✗', + info: 'ℹ', +}; + +const THEME_STORAGE_KEY = 'oai_registrar_theme_v1'; + +function initThemeSwitch() { + const btn = DOM.themeToggleBtn; + if (!btn) return; + + let saved = 'dark'; + try { + const value = localStorage.getItem(THEME_STORAGE_KEY); + if (value === 'light' || value === 'dark') saved = value; + } catch { } + + applyTheme(saved); + + btn.addEventListener('click', () => { + const isLight = document.body.classList.contains('theme-light'); + const nextTheme = isLight ? 'dark' : 'light'; + applyTheme(nextTheme); + try { localStorage.setItem(THEME_STORAGE_KEY, nextTheme); } catch { } + }); +} + +function applyTheme(theme) { + const isLight = theme === 'light'; + document.body.classList.toggle('theme-light', isLight); + updateThemeToggleLabel(isLight); +} + +function updateThemeToggleLabel(isLight) { + const btn = DOM.themeToggleBtn; + if (!btn) return; + const currentLabel = isLight ? '\u660e\u4eae' : '\u9ed1\u6697'; + const nextLabel = isLight ? '\u9ed1\u6697' : '\u660e\u4eae'; + const toggleLabel = btn.querySelector('.theme-toggle-label'); + if (toggleLabel) toggleLabel.textContent = currentLabel; + btn.setAttribute('aria-label', `\u5207\u6362\u5230${nextLabel}\u4e3b\u9898`); + btn.setAttribute('title', `\u5207\u6362\u5230${nextLabel}\u4e3b\u9898`); +} + +function showToast(msg, type = 'info') { + const container = $('toastContainer'); + const toast = document.createElement('div'); + toast.className = `toast ${type}`; + const iconHtml = TOAST_ICONS[type] || TOAST_ICONS.info; + toast.innerHTML = `${iconHtml}${escapeHtml(msg)}`; + container.appendChild(toast); + setTimeout(() => { + toast.style.animation = 'toast-out .25s var(--ease-spring) forwards'; + toast.addEventListener('animationend', () => toast.remove()); + }, 3200); +} + +// ========================================== +// 工具函数 +// ========================================== +function escapeHtml(str) { + return String(str).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"').replace(/'/g, '''); +} + +function cssEscape(str) { + return str.replace(/[^a-zA-Z0-9_-]/g, '_'); +} + +// ========================================== +// 拖拽调整栏宽度 + localStorage 持久化 +// ========================================== +(function initResizable() { + const STORAGE_KEY = 'oai_registrar_layout_v3'; + const shell = document.querySelector('.app-shell'); + const resizeLeft = document.getElementById('resizeLeft'); + const resizeRight = document.getElementById('resizeRight'); + if (!shell) return; + + function getTrackPx(index) { + const tracks = getComputedStyle(shell).gridTemplateColumns.match(/[\d.]+px/g) || []; + const val = tracks[index] ? parseFloat(tracks[index]) : NaN; + return Number.isFinite(val) ? val : NaN; + } + + function loadLayout() { + try { + const saved = JSON.parse(localStorage.getItem(STORAGE_KEY)); + if (!saved) return; + const maxW = shell.getBoundingClientRect().width || window.innerWidth; + if (saved.left && saved.left >= 220 && saved.left <= maxW * 0.4) { + shell.style.setProperty('--col-left', saved.left + 'px'); + } + if (saved.right && saved.right >= 260 && saved.right <= maxW * 0.4) { + shell.style.setProperty('--col-right', saved.right + 'px'); + } + } catch { } + } + + function saveLayout() { + const left = getTrackPx(0); + const right = getTrackPx(4); + const data = {}; + if (Number.isFinite(left) && left > 0) data.left = left; + if (Number.isFinite(right) && right > 0) data.right = right; + if (Object.keys(data).length) { + try { localStorage.setItem(STORAGE_KEY, JSON.stringify(data)); } catch { } + } + } + + function initHandle(handle, prop, minW, getStart) { + if (!handle) return; + handle.addEventListener('mousedown', (e) => { + e.preventDefault(); + document.body.classList.add('resizing'); + handle.classList.add('active'); + const startX = e.clientX; + const startVal = getStart(); + const totalW = shell.getBoundingClientRect().width; + + const onMove = (ev) => { + const dx = ev.clientX - startX; + const delta = prop === '--col-left' ? dx : -dx; + shell.style.setProperty(prop, Math.max(minW, Math.min(startVal + delta, totalW * 0.4)) + 'px'); + }; + const onUp = () => { + document.body.classList.remove('resizing'); + handle.classList.remove('active'); + document.removeEventListener('mousemove', onMove); + document.removeEventListener('mouseup', onUp); + saveLayout(); + }; + document.addEventListener('mousemove', onMove); + document.addEventListener('mouseup', onUp); + }); + } + + initHandle(resizeLeft, '--col-left', 220, () => getTrackPx(0) || 280); + initHandle(resizeRight, '--col-right', 260, () => getTrackPx(4) || 340); + + loadLayout(); +})(); diff --git a/openai_pool_orchestrator/static/index.html b/openai_pool_orchestrator/static/index.html new file mode 100755 index 0000000..9dcb401 --- /dev/null +++ b/openai_pool_orchestrator/static/index.html @@ -0,0 +1,694 @@ + + + + + + + OpenAI Pool Orchestrator + + + + + + + +
+
+ +

Pool Orchestrator

+ v5.2.1 +
+ +
+
+ Sub2Api + -- / -- + -- +
+
+
+
+
+ CPA + -- / -- + -- +
+
+
+
+
+ Local Token + -- / -- + -- +
+
+
+
+
+ + 空闲 +
+ +
+
+ + +
+
+
+ + +
+ +
+ + + + + +
+ + +
+
+
+
+ + 实时日志 + 0 +
+
+ + +
+
+
+
+ 等待任务启动... +
+
+
+
+ + +
+ + + + +
+ +
+ + +
+
+ + +
+
+ 全局上传策略 + +
+
+
+ + +
+ 串行模式更保守;并行模式会让单账号并发上传到两个平台。当前任务运行中切换策略,将在下轮任务生效。 +
+
+
+ + +
+
+
+ + +
+
+ 请求代理池配置 + +
+
+
+ 在每次请求前动态取号。默认接口将附带 api_keycountcountry 参数。 +
+
+ +
+
+ + +
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+
+ + + +
+
+
+ + +
+
+ CPA 平台配置 + +
+
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+
+
+
+ +
+
+ + +
+
+
+ + + +
+
+
+ + +
+
+ Sub2Api 平台配置 + +
+
+
+ + +
+
+
+ + +
+
+ + +
+
+
+ +
+
+
+ + +
+
+ + +
+
+
+ +
+
+ +
+ 手动维护和自动维护都会按勾选项执行,可分别控制异常账号测活、仍异常删除与重复账号清理。 +
+
+ + + +
+
+
+ + + +
+
+
+ + +
+
+ 邮箱提供商 + +
+
+
+
+ +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
+ + + +
+
+
+ +
+
+ + +
+ + + + + diff --git a/openai_pool_orchestrator/static/style.css b/openai_pool_orchestrator/static/style.css new file mode 100755 index 0000000..3e7b95b --- /dev/null +++ b/openai_pool_orchestrator/static/style.css @@ -0,0 +1,2226 @@ +/* ========================================== + OpenAI Pool Orchestrator — iOS Flat Design v2.1 + Modern · Flat · iOS Switch Style + ========================================== */ + +@import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap'); + +/* ---------- Design Tokens ---------- */ +:root { + /* Surfaces — layered depth */ + --bg-base: #000000; + --bg-surface: #1c1c1e; + --bg-card: #2c2c2e; + --bg-elevated: #3a3a3c; + --bg-hover: rgba(255,255,255,.06); + --bg-inset: rgba(0,0,0,.25); + + /* Borders — subtle definition */ + --border: rgba(255,255,255,.08); + --border-card: rgba(255,255,255,.06); + --border-focused: rgba(10,132,255,.5); + --separator: rgba(255,255,255,.05); + + /* iOS System Colors */ + --accent-blue: #0a84ff; + --accent-blue-dim: rgba(10,132,255,.15); + --accent-green: #30d158; + --accent-green-dim: rgba(48,209,88,.12); + --accent-red: #ff453a; + --accent-red-dim: rgba(255,69,58,.12); + --accent-yellow: #ffd60a; + --accent-orange: #ff9f0a; + --accent-orange-dim: rgba(255,159,10,.12); + --accent-purple: #bf5af2; + --accent-teal: #64d2ff; + --accent-teal-dim: rgba(100,210,255,.12); + + /* Text */ + --text-primary: rgba(255,255,255,.92); + --text-secondary: rgba(255,255,255,.55); + --text-muted: rgba(255,255,255,.30); + + /* Radius — iOS rounded corners */ + --radius-xs: 6px; + --radius-sm: 8px; + --radius-md: 12px; + --radius-lg: 16px; + --radius-xl: 20px; + --radius-pill: 999px; + + /* Shadows */ + --shadow-card: 0 1px 3px rgba(0,0,0,.3), 0 0 0 1px var(--border-card); + --shadow-card-hover: 0 4px 16px rgba(0,0,0,.4), 0 0 0 1px rgba(255,255,255,.1); + --shadow-elevated: 0 8px 30px rgba(0,0,0,.5); + --shadow-button: 0 1px 2px rgba(0,0,0,.3); + --shadow-glow-blue: 0 0 20px rgba(10,132,255,.15); + --shadow-glow-green: 0 0 20px rgba(48,209,88,.15); + --shadow-glow-red: 0 0 20px rgba(255,69,58,.15); + + /* Transitions */ + --ease-spring: cubic-bezier(.4, 0, .2, 1); + --ease-bounce: cubic-bezier(.34, 1.56, .64, 1); + --ease-out: cubic-bezier(0, 0, .2, 1); + --duration-fast: .15s; + --duration-normal: .25s; + --duration-slow: .4s; + --watermark-svg: url("data:image/svg+xml,%3Csvg%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%20width%3D%27420%27%20height%3D%27240%27%20viewBox%3D%270%200%20420%20240%27%3E%0A%20%20%3Cdefs%3E%0A%20%20%20%20%3CclipPath%20id%3D%27ld_clip_wm%27%3E%0A%20%20%20%20%20%20%3Ccircle%20cx%3D%2760%27%20cy%3D%2760%27%20r%3D%2747%27%3E%3C%2Fcircle%3E%0A%20%20%20%20%3C%2FclipPath%3E%0A%20%20%3C%2Fdefs%3E%0A%20%20%3Cg%20transform%3D%27rotate%28-22%20210%20120%29%27%3E%0A%20%20%20%20%3Cg%20transform%3D%27translate%2878%2078%29%20scale%280.37%29%27%20opacity%3D%270.20%27%3E%0A%20%20%20%20%20%20%3Ccircle%20fill%3D%27%23f0f0f0%27%20cx%3D%2760%27%20cy%3D%2760%27%20r%3D%2750%27%3E%3C%2Fcircle%3E%0A%20%20%20%20%20%20%3Crect%20fill%3D%27%231c1c1e%27%20clip-path%3D%27url%28%23ld_clip_wm%29%27%20x%3D%2710%27%20y%3D%2710%27%20width%3D%27100%27%20height%3D%2730%27%3E%3C%2Frect%3E%0A%20%20%20%20%20%20%3Crect%20fill%3D%27%23f0f0f0%27%20clip-path%3D%27url%28%23ld_clip_wm%29%27%20x%3D%2710%27%20y%3D%2740%27%20width%3D%27100%27%20height%3D%2740%27%3E%3C%2Frect%3E%0A%20%20%20%20%20%20%3Crect%20fill%3D%27%23ffb003%27%20clip-path%3D%27url%28%23ld_clip_wm%29%27%20x%3D%2710%27%20y%3D%2780%27%20width%3D%27100%27%20height%3D%2730%27%3E%3C%2Frect%3E%0A%20%20%20%20%3C%2Fg%3E%0A%20%20%20%20%3Ctext%20x%3D%27136%27%20y%3D%27108%27%20fill%3D%27rgba%28255%2C255%2C255%2C0.10%29%27%20font-family%3D%27Arial%2C%20Microsoft%20YaHei%2C%20sans-serif%27%20font-size%3D%2724%27%20font-weight%3D%27700%27%20letter-spacing%3D%271.8%27%3ELINUX%20DO%20%E7%A4%BE%E5%8C%BA%3C%2Ftext%3E%0A%20%20%3C%2Fg%3E%0A%3C%2Fsvg%3E"); +} + +body.theme-light { + --bg-base: #f2f2f3; + --bg-surface: #ffffff; + --bg-card: #f7f7f8; + --bg-elevated: #ffffff; + --bg-hover: rgba(0,0,0,.04); + --bg-inset: rgba(0,0,0,.04); + + --border: rgba(0,0,0,.12); + --border-card: rgba(0,0,0,.08); + --border-focused: rgba(0,0,0,.34); + --separator: rgba(0,0,0,.08); + + --accent-blue: #111111; + --accent-blue-dim: rgba(0,0,0,.10); + --accent-green: #0f9f57; + --accent-green-dim: rgba(15,159,87,.14); + --accent-red: #d73a2f; + --accent-red-dim: rgba(215,58,47,.12); + --accent-yellow: #b88a00; + --accent-orange: #c27600; + --accent-orange-dim: rgba(194,118,0,.12); + --accent-purple: #444444; + --accent-teal: #353535; + --accent-teal-dim: rgba(53,53,53,.12); + + --text-primary: rgba(17,17,17,.92); + --text-secondary: rgba(17,17,17,.64); + --text-muted: rgba(17,17,17,.42); + + --shadow-card: 0 1px 2px rgba(0,0,0,.06), 0 0 0 1px var(--border-card); + --shadow-card-hover: 0 4px 14px rgba(0,0,0,.08), 0 0 0 1px rgba(0,0,0,.1); + --shadow-elevated: 0 12px 34px rgba(0,0,0,.10); + --shadow-button: 0 1px 2px rgba(0,0,0,.12); + --shadow-glow-blue: none; + --shadow-glow-green: none; + --shadow-glow-red: none; + --watermark-svg: url("data:image/svg+xml,%3Csvg%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%20width%3D%27420%27%20height%3D%27240%27%20viewBox%3D%270%200%20420%20240%27%3E%0A%20%20%3Cdefs%3E%0A%20%20%20%20%3CclipPath%20id%3D%27ld_clip_wm%27%3E%0A%20%20%20%20%20%20%3Ccircle%20cx%3D%2760%27%20cy%3D%2760%27%20r%3D%2747%27%3E%3C%2Fcircle%3E%0A%20%20%20%20%3C%2FclipPath%3E%0A%20%20%3C%2Fdefs%3E%0A%20%20%3Cg%20transform%3D%27rotate%28-22%20210%20120%29%27%3E%0A%20%20%20%20%3Cg%20transform%3D%27translate%2878%2078%29%20scale%280.37%29%27%20opacity%3D%270.16%27%3E%0A%20%20%20%20%20%20%3Ccircle%20fill%3D%27%23f0f0f0%27%20cx%3D%2760%27%20cy%3D%2760%27%20r%3D%2750%27%3E%3C%2Fcircle%3E%0A%20%20%20%20%20%20%3Crect%20fill%3D%27%231c1c1e%27%20clip-path%3D%27url%28%23ld_clip_wm%29%27%20x%3D%2710%27%20y%3D%2710%27%20width%3D%27100%27%20height%3D%2730%27%3E%3C%2Frect%3E%0A%20%20%20%20%20%20%3Crect%20fill%3D%27%23f0f0f0%27%20clip-path%3D%27url%28%23ld_clip_wm%29%27%20x%3D%2710%27%20y%3D%2740%27%20width%3D%27100%27%20height%3D%2740%27%3E%3C%2Frect%3E%0A%20%20%20%20%20%20%3Crect%20fill%3D%27%23ffb003%27%20clip-path%3D%27url%28%23ld_clip_wm%29%27%20x%3D%2710%27%20y%3D%2780%27%20width%3D%27100%27%20height%3D%2730%27%3E%3C%2Frect%3E%0A%20%20%20%20%3C%2Fg%3E%0A%20%20%20%20%3Ctext%20x%3D%27136%27%20y%3D%27108%27%20fill%3D%27rgba%280%2C0%2C0%2C0.10%29%27%20font-family%3D%27Arial%2C%20Microsoft%20YaHei%2C%20sans-serif%27%20font-size%3D%2724%27%20font-weight%3D%27700%27%20letter-spacing%3D%271.8%27%3ELINUX%20DO%20%E7%A4%BE%E5%8C%BA%3C%2Ftext%3E%0A%20%20%3C%2Fg%3E%0A%3C%2Fsvg%3E"); +} + +/* ---------- Reset ---------- */ +*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } +html, body { height: 100%; } + +body { + font-family: 'Plus Jakarta Sans', -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'Segoe UI', system-ui, sans-serif; + font-size: 14px; + background: var(--bg-base); + color: var(--text-primary); + line-height: 1.5; + overflow: hidden; + display: flex; + flex-direction: column; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +body::before { + content: ''; + position: fixed; + inset: 0; + z-index: 999; + pointer-events: none; + background-image: var(--watermark-svg); + background-repeat: repeat; + background-size: 420px 240px; +} + +/* ---------- Scrollbar — Thin & Subtle ---------- */ +* { scrollbar-width: thin; scrollbar-color: rgba(255,255,255,.1) transparent; } +::-webkit-scrollbar { width: 4px; } +::-webkit-scrollbar-track { background: transparent; } +::-webkit-scrollbar-thumb { background: rgba(255,255,255,.1); border-radius: 9px; } +::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,.18); } + +/* ========================================== + HEADER — Glassmorphism Bar + ========================================== */ +header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + padding: 0 20px; + min-height: 52px; + background: rgba(28,28,30,.82); + backdrop-filter: saturate(180%) blur(20px); + -webkit-backdrop-filter: saturate(180%) blur(20px); + border-bottom: 1px solid var(--border); + position: relative; + z-index: 10; + flex-shrink: 0; +} + +.header-brand { + display: flex; + align-items: center; + gap: 10px; +} + +.header-brand .logo { + width: 30px; + height: 30px; + background: linear-gradient(135deg, var(--accent-blue) 0%, #409cff 100%); + border-radius: 8px; + display: flex; + align-items: center; + justify-content: center; + color: #fff; + box-shadow: 0 2px 8px rgba(10,132,255,.35); +} + +.header-brand h1 { + font-size: 15px; + font-weight: 700; + color: var(--text-primary); + letter-spacing: -.3px; +} + +.header-brand .version { + font-size: 10px; + font-weight: 600; + color: var(--accent-blue); + background: var(--accent-blue-dim); + padding: 2px 8px; + border-radius: var(--radius-pill); + letter-spacing: .3px; +} + +.header-right { + display: flex; + align-items: center; + justify-content: flex-end; + gap: 8px; + flex-shrink: 0; + min-width: 0; +} + +.theme-toggle-btn { + height: 34px; + min-width: 68px; + padding: 0 10px; + border: 1px solid var(--border-card); + border-radius: var(--radius-pill); + background: var(--bg-card); + color: var(--text-primary); + display: inline-flex; + align-items: center; + justify-content: center; + gap: 8px; + font-size: 12px; + font-weight: 700; + cursor: pointer; + transition: all var(--duration-fast) var(--ease-spring); +} + +.theme-toggle-btn:hover { + border-color: rgba(255,255,255,.14); + background: var(--bg-elevated); +} + +.theme-toggle-btn:active { + transform: scale(.98); +} + +.theme-toggle-icon { + width: 14px; + height: 14px; + border-radius: 50%; + border: 2px solid currentColor; + position: relative; +} + +.theme-toggle-icon::after { + content: ''; + position: absolute; + inset: 2px; + border-radius: 50%; + background: currentColor; + opacity: .25; +} + +.theme-toggle-label { + letter-spacing: .3px; +} + +/* ---------- Status Badge ---------- */ +.status-badge { + display: flex; + align-items: center; + gap: 6px; + padding: 5px 14px; + border-radius: var(--radius-pill); + font-size: 12px; + font-weight: 600; + background: var(--bg-card); + border: 1px solid var(--border-card); + transition: all .3s var(--ease-spring); +} + +.status-dot { + width: 7px; + height: 7px; + border-radius: 50%; + background: var(--text-muted); + transition: all .3s; +} + +.status-badge.running { + background: var(--accent-green-dim); + border-color: rgba(48,209,88,.2); +} +.status-badge.running .status-dot { + background: var(--accent-green); + box-shadow: 0 0 8px var(--accent-green); + animation: pulse-dot 1.8s infinite; +} + +.status-badge.stopping { + background: var(--accent-orange-dim); + border-color: rgba(255,159,10,.2); +} +.status-badge.stopping .status-dot { + background: var(--accent-orange); +} + +@keyframes pulse-dot { + 0%, 100% { opacity: 1; transform: scale(1); } + 50% { opacity: .5; transform: scale(1.4); } +} + +/* ---------- Pool Chips — Mini Dashboard ---------- */ +.pool-chip { + height: 46px; + min-width: 148px; + border-radius: var(--radius-md); + background: var(--bg-card); + border: 1px solid var(--border-card); + display: grid; + grid-template-columns: 1fr auto; + grid-template-rows: 14px 17px 3px; + grid-template-areas: "name delta" "value delta" "track track"; + align-items: end; + gap: 2px 8px; + padding: 6px 10px 5px; + overflow: hidden; + white-space: nowrap; + transition: all .25s var(--ease-spring); +} +.pool-chip:hover { border-color: rgba(255,255,255,.12); } + +.pool-chip.interactive-chip { + cursor: pointer; + user-select: none; +} + +.pool-chip.interactive-chip.active-view { + border-color: rgba(10,132,255,.32); + box-shadow: 0 0 0 1px rgba(10,132,255,.22), 0 8px 18px rgba(10,132,255,.14); +} + +.pool-chip.interactive-chip:focus-visible { + outline: 2px solid var(--accent-blue); + outline-offset: 2px; +} + +.pool-chip-name { grid-area: name; font-size: 10px; font-weight: 600; color: var(--text-muted); letter-spacing: .4px; text-transform: uppercase; line-height: 1; } +.pool-chip-value { grid-area: value; font-size: 13px; font-weight: 700; color: var(--text-primary); white-space: nowrap; font-family: 'JetBrains Mono', monospace; line-height: 1; } +.pool-chip-delta { grid-area: delta; font-size: 11px; font-weight: 700; line-height: 1; padding: 3px 7px; border-radius: var(--radius-sm); background: rgba(255,255,255,.06); color: var(--text-secondary); align-self: center; } +.pool-chip-track { grid-area: track; height: 3px; border-radius: 9px; background: rgba(255,255,255,.06); overflow: hidden; } +.pool-chip-fill { height: 100%; border-radius: 9px; background: var(--accent-green); transition: width .4s var(--ease-spring), background .2s; } +.pool-chip-fill.warn { background: var(--accent-orange); } +.pool-chip-fill.danger { background: var(--accent-red); } +.pool-chip-fill.ok { background: var(--accent-green); } +.pool-chip-fill.over { background: var(--accent-teal); } +.pool-chip.status-warn { background: rgba(255,159,10,.06); border-color: rgba(255,159,10,.15); } +.pool-chip.status-danger { background: rgba(255,69,58,.06); border-color: rgba(255,69,58,.15); } +.pool-chip.status-ok { background: rgba(48,209,88,.04); border-color: rgba(48,209,88,.12); } +.pool-chip.status-over { background: rgba(100,210,255,.04); border-color: rgba(100,210,255,.12); } +.pool-chip-delta.warn { color: var(--accent-orange); background: var(--accent-orange-dim); } +.pool-chip-delta.danger { color: var(--accent-red); background: var(--accent-red-dim); } +.pool-chip-delta.ok { color: var(--accent-green); background: var(--accent-green-dim); } +.pool-chip-delta.over { color: var(--accent-teal); background: var(--accent-teal-dim); } + +/* ========================================== + PROGRESS BAR + ========================================== */ +.progress-bar { + height: 2px; + background: var(--bg-surface); + position: relative; + overflow: hidden; + flex-shrink: 0; +} + +.progress-fill { + height: 100%; + background: linear-gradient(90deg, var(--accent-blue), var(--accent-teal)); + width: 0; + transition: width .5s var(--ease-spring); +} + +.progress-fill.running { + animation: progress-slide 1.4s infinite linear; + width: 35%; +} + +.progress-fill.stopping { + width: 100%; + background: linear-gradient(90deg, var(--accent-orange), var(--accent-red)); + opacity: .78; +} + +@keyframes progress-slide { + 0% { transform: translateX(-100%); } + 100% { transform: translateX(380%); } +} + +/* ========================================== + TAB NAVIGATION — True iOS Segmented Control + ========================================== */ +.tab-nav { + display: flex; + align-items: center; + justify-content: center; + padding: 0 12px; + background: transparent; + border-bottom: none; + flex-shrink: 0; +} + +.header-tab-nav { + flex: 1; + min-width: 0; +} + +.header-tab-nav .segmented-control { + max-width: 100%; +} + +.segmented-control { + display: inline-flex; + align-items: center; + background: rgba(118,118,128,.24); + border-radius: 9px; + padding: 2px; + position: relative; + gap: 0; +} + +.segment-indicator { + position: absolute; + top: 2px; + left: 2px; + height: calc(100% - 4px); + width: calc(50% - 2px); + background: var(--bg-elevated); + border-radius: 7px; + transition: transform var(--duration-normal) var(--ease-spring); + z-index: 0; + box-shadow: 0 1px 3px rgba(0,0,0,.2), 0 0 0 .5px rgba(0,0,0,.1); +} + +.segment-indicator[data-active="1"] { + transform: translateX(calc(100% + 2px)); +} + +.tab-btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 6px; + padding: 6px 24px; + border-radius: 7px; + font-size: 13px; + font-weight: 600; + cursor: pointer; + border: none; + background: transparent; + color: var(--text-secondary); + transition: color var(--duration-fast) var(--ease-spring); + position: relative; + z-index: 1; + min-width: 120px; + user-select: none; +} + +.tab-btn svg { opacity: .7; transition: opacity var(--duration-fast); } +.tab-btn:hover { color: var(--text-primary); } +.tab-btn:hover svg { opacity: .9; } +.tab-btn.active { color: var(--text-primary); } +.tab-btn.active svg { opacity: 1; } + +/* ========================================== + TAB PANELS — Fade Transition + ========================================== */ +.tab-panel { + display: none; + flex: 1; + min-height: 0; + overflow: hidden; + animation: tab-fade-in .2s var(--ease-out); +} +.tab-panel.active { display: flex; flex-direction: column; } + +@keyframes tab-fade-in { + from { opacity: 0; } + to { opacity: 1; } +} + +/* ========================================== + THREE-COLUMN LAYOUT + ========================================== */ +.app-shell { + display: grid; + grid-template-columns: var(--col-left, 280px) 4px 1fr 4px var(--col-right, 340px); + flex: 1; + min-height: 0; + overflow: hidden; +} + +/* ---------- Resize Handle ---------- */ +.resize-handle { + width: 4px; + cursor: col-resize; + background: transparent; + transition: background var(--duration-fast); + position: relative; + z-index: 5; + flex-shrink: 0; +} +.resize-handle::after { + content: ''; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 2px; + height: 24px; + border-radius: 1px; + background: rgba(255,255,255,.08); + opacity: 0; + transition: opacity var(--duration-normal); +} +.resize-handle:hover::after, .resize-handle.active::after { + opacity: 1; + background: var(--accent-blue); +} +.resize-handle:hover, .resize-handle.active { + background: rgba(10,132,255,.2); +} +body.resizing { cursor: col-resize !important; user-select: none !important; -webkit-user-select: none !important; } +body.resizing * { cursor: col-resize !important; } + +/* ========================================== + LEFT SIDEBAR + ========================================== */ +.sidebar { + background: var(--bg-surface); + border-right: 1px solid var(--border); + display: flex; + flex-direction: column; + overflow-y: auto; + overflow-x: hidden; + min-width: 0; +} + +.panel-section { + padding: 12px 12px 10px; + border-bottom: 1px solid var(--separator); +} +.panel-section:last-child { border-bottom: none; } + +.section-title { + font-size: 10px; + font-weight: 700; + color: var(--text-muted); + text-transform: uppercase; + letter-spacing: .6px; + margin-bottom: 8px; +} + +/* ========================================== + iOS TOGGLE SWITCH — Refined + ========================================== */ +.ios-toggle { + position: relative; + display: inline-block; + width: 44px; + height: 26px; + flex-shrink: 0; +} + +.ios-toggle input { + opacity: 0; + width: 0; + height: 0; + position: absolute; +} + +.ios-toggle .toggle-track { + position: absolute; + inset: 0; + background: rgba(120, 120, 128, .36); + border-radius: 13px; + cursor: pointer; + transition: background .3s var(--ease-spring); +} + +.ios-toggle .toggle-track::after { + content: ''; + position: absolute; + top: 2px; + left: 2px; + width: 22px; + height: 22px; + background: #fff; + border-radius: 50%; + transition: transform .3s var(--ease-spring), box-shadow .2s; + box-shadow: 0 2px 4px rgba(0,0,0,.25), 0 0 1px rgba(0,0,0,.15); +} + +.ios-toggle input:checked + .toggle-track { + background: var(--accent-green); +} + +.ios-toggle input:checked + .toggle-track::after { + transform: translateX(18px); + box-shadow: 0 2px 4px rgba(48,209,88,.3), 0 0 1px rgba(0,0,0,.1); +} + +/* Small variant */ +.ios-toggle-sm { + width: 38px; + height: 22px; +} + +.ios-toggle-sm .toggle-track { + border-radius: 11px; +} + +.ios-toggle-sm .toggle-track::after { + width: 18px; + height: 18px; + top: 2px; + left: 2px; +} + +.ios-toggle-sm input:checked + .toggle-track::after { + transform: translateX(16px); +} + +.ios-toggle-label { + display: flex; + align-items: center; + gap: 8px; + font-size: 12px; + cursor: pointer; + color: var(--text-primary); + user-select: none; +} + +.toggle-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + margin-bottom: 8px; +} + +.thread-count-wrap { + display: flex; + align-items: center; + gap: 5px; + font-size: 11px; + color: var(--text-secondary); +} + +.thread-count-wrap input { + width: 52px; + padding: 4px 6px; + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: var(--radius-sm); + color: var(--text-primary); + font-size: 12px; + text-align: center; + outline: none; + font-family: 'JetBrains Mono', monospace; + transition: border-color var(--duration-fast), box-shadow var(--duration-fast); +} + +.thread-count-wrap input:focus { + border-color: var(--accent-blue); + box-shadow: 0 0 0 3px var(--accent-blue-dim); +} + +/* ========================================== + FORM INPUTS — iOS Style + ========================================== */ +.proxy-row { + display: flex; + gap: 5px; + margin-bottom: 6px; +} + +.sidebar input[type="text"], +.sidebar input[type="password"] { + font-size: 11px; + padding: 8px 10px; +} + +.input-wrapper { flex: 1; position: relative; } + +input[type="text"], +input[type="password"] { + width: 100%; + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: var(--radius-sm); + color: var(--text-primary); + font-family: 'JetBrains Mono', monospace; + font-size: 12px; + padding: 9px 12px; + outline: none; + transition: border-color var(--duration-fast), box-shadow var(--duration-fast), background var(--duration-fast); + box-sizing: border-box; + min-width: 0; +} + +input[type="text"]:focus, +input[type="password"]:focus { + border-color: var(--accent-blue); + box-shadow: 0 0 0 3px var(--accent-blue-dim); + background: rgba(10,132,255,.03); +} + +input::placeholder { color: var(--text-muted); } + +.proxy-status { + font-size: 11px; + padding: 7px 10px; + border-radius: var(--radius-sm); + background: var(--bg-card); + border: 1px solid var(--border-card); + color: var(--text-muted); + min-height: 30px; + display: flex; + align-items: center; + gap: 6px; + transition: all .3s var(--ease-spring); + margin-top: 4px; +} +.proxy-status.ok { color: var(--accent-green); background: var(--accent-green-dim); border-color: rgba(48,209,88,.2); } +.proxy-status.fail { color: var(--accent-red); background: var(--accent-red-dim); border-color: rgba(255,69,58,.2); } + +/* ========================================== + BUTTONS — Flat iOS Style with Depth + ========================================== */ +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 6px; + padding: 8px 16px; + border-radius: var(--radius-sm); + font-size: 13px; + font-weight: 600; + cursor: pointer; + border: none; + transition: all var(--duration-fast) var(--ease-spring); + white-space: nowrap; + user-select: none; + font-family: inherit; + position: relative; + overflow: hidden; +} + +.btn:active { transform: scale(.96); } +.btn:disabled { opacity: .35; cursor: not-allowed; transform: none !important; } + +.btn-sm { padding: 6px 12px; font-size: 12px; } + +.btn-ghost { + background: var(--bg-card); + color: var(--text-secondary); + border: 1px solid var(--border-card); +} +.btn-ghost:hover { + background: var(--bg-elevated); + color: var(--text-primary); + border-color: rgba(255,255,255,.12); +} + +.btn-primary { + background: var(--accent-blue); + color: #fff; + box-shadow: var(--shadow-button); +} +.btn-primary:hover { background: #409cff; box-shadow: var(--shadow-glow-blue); } + +.btn-danger { + background: var(--accent-red-dim); + color: var(--accent-red); + border: 1px solid rgba(255,69,58,.15); +} +.btn-danger:hover { background: rgba(255,69,58,.2); border-color: rgba(255,69,58,.25); } + +.btn-success { + background: var(--accent-green); + color: #fff; + box-shadow: var(--shadow-button); +} +.btn-success:hover { background: #3bdf66; box-shadow: var(--shadow-glow-green); } + +.control-buttons { + display: flex; + gap: 6px; +} +.control-buttons .btn { + flex: 1; + padding: 8px; + border-radius: var(--radius-md); + font-size: 13px; +} +.control-buttons .btn svg { + flex-shrink: 0; +} + +.sidebar .btn-sm { + padding: 5px 10px; + font-size: 11px; +} + +/* ========================================== + STATS CARDS — Subtle Glow + ========================================== */ +.stats-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 6px; + align-items: stretch; +} + +.stat-card { + background: var(--bg-card); + border-radius: var(--radius-md); + padding: 10px 8px 9px; + text-align: center; + transition: all .2s var(--ease-spring); + border: 1px solid var(--border-card); + position: relative; + overflow: hidden; + min-width: 0; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 5px; +} +.stat-card::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 2px; + border-radius: 2px 2px 0 0; + opacity: 0; + transition: opacity .2s; +} +.stat-card:hover { background: var(--bg-elevated); border-color: rgba(255,255,255,.1); } +.stat-card:hover::before { opacity: 1; } +.stat-card.span-2 { grid-column: auto; } + +.stat-value { + font-size: 20px; + font-weight: 800; + font-family: 'JetBrains Mono', monospace; + letter-spacing: -.6px; + line-height: .95; + white-space: nowrap; +} +.stat-value.green { color: var(--accent-green); } +.stat-value.red { color: var(--accent-red); } +.stat-value.blue { color: var(--accent-blue); } +.stat-value.muted { color: var(--text-muted); } + +/* Top accent lines for stat cards */ +.stat-card:nth-child(1)::before { background: var(--accent-green); } +.stat-card:nth-child(2)::before { background: var(--accent-red); } +.stat-card:nth-child(3)::before { background: var(--accent-blue); } + +.stat-label { + font-size: 9px; + font-weight: 600; + color: var(--text-muted); + margin-top: 0; + text-transform: uppercase; + letter-spacing: .3px; + line-height: 1.1; +} + +.progress-section-block { + display: flex; + flex-direction: column; + gap: 8px; +} + +.progress-section-block + .progress-section-block { + margin-top: 12px; +} + +.progress-subtitle { + font-size: 10px; + font-weight: 700; + letter-spacing: .35px; + text-transform: uppercase; + color: var(--text-secondary); +} + +.progress-subtitle-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; +} + +.task-overview-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 6px; +} + +.task-overview-card { + padding: 8px 10px; + background: var(--bg-card); + border: 1px solid var(--border-card); + border-radius: var(--radius-md); + display: flex; + flex-direction: column; + gap: 5px; + min-width: 0; +} + +.task-overview-card.empty { + grid-column: 1 / -1; + color: var(--text-muted); +} + +.task-overview-card.task-status-running, +.task-overview-card.task-status-preparing { + border-color: rgba(10,132,255,.25); + background: linear-gradient(180deg, rgba(10,132,255,.12), rgba(10,132,255,.04)); +} + +.task-overview-card.task-status-stopping { + border-color: rgba(255,159,10,.25); + background: linear-gradient(180deg, rgba(255,159,10,.12), rgba(255,159,10,.04)); +} + +.task-overview-card.task-status-idle, +.task-overview-card.task-status-meta { + border-color: var(--border-card); +} + +.task-overview-label { + font-size: 9px; + font-weight: 700; + letter-spacing: .35px; + text-transform: uppercase; + color: var(--text-muted); +} + +.task-overview-value { + font-size: 16px; + font-weight: 700; + color: var(--text-primary); + font-family: 'JetBrains Mono', monospace; + word-break: break-word; +} + +.task-overview-hint { + font-size: 10px; + color: var(--text-secondary); +} + +.worker-list { + display: flex; + flex-direction: column; + gap: 6px; + max-height: 228px; + overflow-y: auto; +} + +.worker-card { + width: 100%; + border: 1px solid var(--border-card); + border-radius: var(--radius-md); + background: var(--bg-card); + padding: 8px 10px; + display: flex; + flex-direction: column; + gap: 6px; + color: var(--text-primary); + cursor: pointer; + transition: border-color .2s var(--ease-spring), transform .2s var(--ease-spring), background .2s var(--ease-spring); +} + +.worker-card:hover { + border-color: var(--border-focused); + transform: translateY(-1px); +} + +.worker-card.focused { + border-color: var(--accent-blue); + box-shadow: 0 0 0 1px rgba(10,132,255,.22); + background: linear-gradient(180deg, rgba(10,132,255,.12), rgba(10,132,255,.04)); +} + +.worker-card.empty { + cursor: default; + color: var(--text-muted); +} + +.worker-card-head, +.worker-card-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; +} + +.worker-card-label { + font-size: 11px; + font-weight: 700; +} + +.worker-card-email { + font-size: 10px; + color: var(--text-secondary); + word-break: break-word; +} + +.worker-card-meta { + font-size: 10px; + color: var(--text-secondary); +} + +.worker-status-badge { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 3px 8px; + border-radius: var(--radius-pill); + font-size: 10px; + font-weight: 700; + letter-spacing: .3px; + text-transform: uppercase; + border: 1px solid transparent; +} + +.worker-status-badge.preparing, +.worker-status-badge.running, +.worker-status-badge.registering { + color: var(--accent-blue); + background: var(--accent-blue-dim); + border-color: rgba(10,132,255,.25); +} + +.worker-status-badge.postprocessing, +.worker-status-badge.waiting, +.worker-status-badge.stopping { + color: var(--accent-orange); + background: var(--accent-orange-dim); + border-color: rgba(255,159,10,.25); +} + +.worker-status-badge.error { + color: var(--accent-red); + background: var(--accent-red-dim); + border-color: rgba(255,69,58,.25); +} + +.worker-status-badge.stopped, +.worker-status-badge.idle { + color: var(--text-secondary); + background: rgba(255,255,255,.06); + border-color: var(--border-card); +} + +.worker-detail-card { + padding: 10px; + background: var(--bg-card); + border: 1px solid var(--border-card); + border-radius: var(--radius-md); + display: flex; + flex-direction: column; + gap: 10px; + min-height: 200px; + max-height: min(68vh, 760px); + overflow: hidden; +} + +.worker-detail-card.empty { + color: var(--text-muted); + display: flex; + align-items: center; + justify-content: center; +} + +.worker-detail-running, +.worker-detail-preparing, +.worker-detail-registering { + border-color: rgba(10,132,255,.25); +} + +.worker-detail-postprocessing, +.worker-detail-waiting, +.worker-detail-stopping { + border-color: rgba(255,159,10,.25); +} + +.worker-detail-error { + border-color: rgba(255,69,58,.25); +} + +.worker-detail-meta { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 8px; +} + +.worker-detail-meta-item { + display: flex; + flex-direction: column; + gap: 6px; + padding: 10px 12px; + background: rgba(255,255,255,.02); + border: 1px solid var(--border-card); + border-radius: var(--radius-sm); + min-width: 0; +} + +.worker-detail-meta-item.wide { + grid-column: 1 / -1; +} + +.worker-detail-meta-label { + font-size: 10px; + font-weight: 700; + letter-spacing: .35px; + text-transform: uppercase; + color: var(--text-muted); +} + +.worker-detail-meta-value { + font-size: 12px; + font-weight: 600; + color: var(--text-primary); + word-break: break-word; +} + +.worker-detail-steps { + display: flex; + flex-direction: column; + gap: 10px; + min-height: 0; + flex: 1 1 auto; + overflow: hidden; +} + +.worker-detail-steps-title { + font-size: 11px; + font-weight: 700; + letter-spacing: .35px; + text-transform: uppercase; + color: var(--text-secondary); +} + +.step-track-list { + display: flex; + flex-direction: column; + gap: 8px; + overflow-y: auto; + padding-right: 4px; +} + +.step-track-item { + position: relative; + padding: 10px 12px 10px 16px; + border-radius: var(--radius-sm); + background: rgba(255,255,255,.02); + border: 1px solid var(--border-card); + display: flex; + flex-direction: column; + gap: 6px; +} + +.step-track-item::before { + content: ''; + position: absolute; + left: 0; + top: 8px; + bottom: 8px; + width: 3px; + border-radius: 999px; + background: rgba(255,255,255,.08); +} + +.step-track-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; +} + +.step-track-label { + font-size: 12px; + font-weight: 700; + color: var(--text-primary); +} + +.step-track-badge { + font-size: 10px; + font-weight: 700; + letter-spacing: .3px; + text-transform: uppercase; + color: var(--text-secondary); +} + +.step-track-message, +.step-track-time, +.step-track-empty { + font-size: 11px; + color: var(--text-secondary); + word-break: break-word; +} + +.step-status-active::before { + background: var(--accent-blue); +} + +.step-status-done::before { + background: var(--accent-green); +} + +.step-status-error::before { + background: var(--accent-red); +} + +.step-status-pending::before, +.step-status-skipped::before { + background: rgba(255,255,255,.18); +} + +@media (max-width: 720px) { + .task-overview-grid, + .worker-detail-meta { + grid-template-columns: 1fr; + } + + .progress-subtitle-row, + .worker-card-head, + .worker-card-row, + .step-track-head { + flex-direction: column; + align-items: stretch; + } +} + +/* ========================================== + LOG PANEL (CENTER) + ========================================== */ +.main-area { display: flex; flex-direction: column; overflow: hidden; min-width: 0; } +.log-panel { display: flex; flex-direction: column; overflow: hidden; flex: 1; } + +.log-header { + padding: 8px 20px; + display: flex; + align-items: center; + justify-content: space-between; + border-bottom: 1px solid var(--border); + background: var(--bg-surface); + flex-shrink: 0; +} + +.log-title { + display: flex; + align-items: center; + gap: 8px; + font-size: 13px; + font-weight: 600; + color: var(--text-primary); +} +.log-title svg { color: var(--text-secondary); } + +.log-count-badge { + font-size: 10px; + font-weight: 700; + color: var(--accent-blue); + background: var(--accent-blue-dim); + padding: 1px 8px; + border-radius: var(--radius-pill); + font-family: 'JetBrains Mono', monospace; +} + +.log-actions { + display: flex; + align-items: center; + gap: 10px; +} + +.log-autoscroll-label { font-size: 11px; gap: 6px; } +.log-autoscroll-label span:last-child { color: var(--text-muted); font-weight: 500; } + +@media (max-width: 720px) { + .task-overview-grid { + grid-template-columns: 1fr; + } + + .progress-subtitle-row { + flex-direction: column; + align-items: stretch; + } +} + +.log-body { + flex: 1; + overflow-y: auto; + overflow-x: hidden; + padding: 8px 0; + background: var(--bg-base); + font-family: 'JetBrains Mono', monospace; + font-size: 12px; + line-height: 1.8; +} + +.log-entry { + display: flex; + align-items: flex-start; + gap: 8px; + padding: 2px 20px; + transition: background .1s; + border-left: 2px solid transparent; +} +.log-entry:hover { background: rgba(255,255,255,.02); } + +.log-placeholder { + color: var(--text-muted); + padding: 20px; + font-family: 'JetBrains Mono', monospace; + font-size: 12px; + text-align: center; +} + +.log-ts { color: var(--text-muted); flex-shrink: 0; font-size: 11px; margin-top: 2px; } +.log-icon { flex-shrink: 0; font-size: 12px; margin-top: 1px; } +.log-msg { color: var(--text-primary); word-break: break-all; flex: 1; min-width: 0; } +.log-msg.info { color: var(--text-secondary); } +.log-msg.success { color: var(--accent-green); } +.log-msg.error { color: var(--accent-red); } +.log-msg.warn { color: var(--accent-orange); } +.log-msg.connected { color: var(--text-muted); font-style: italic; } + +/* Log entry left accent by type */ +.log-entry:has(.log-msg.success) { border-left-color: rgba(48,209,88,.3); } +.log-entry:has(.log-msg.error) { border-left-color: rgba(255,69,58,.3); } +.log-entry:has(.log-msg.warn) { border-left-color: rgba(255,159,10,.3); } + +.log-step { + font-size: 9px; + font-weight: 600; + color: var(--text-muted); + background: var(--bg-surface); + padding: 1px 6px; + border-radius: 4px; + flex-shrink: 0; + margin-top: 3px; +} + +/* ========================================== + RIGHT DATA PANEL + ========================================== */ +.data-panel { + background: var(--bg-surface); + border-left: 1px solid var(--border); + display: flex; + flex-direction: column; + overflow: hidden; + min-width: 0; +} + +.remote-panel-wrap { + flex: 1 1 auto; + min-height: 0; + display: flex; + flex-direction: column; + overflow: hidden; + background: var(--bg-base); + border-bottom: 1px solid var(--border); +} + +.data-panel-body { + flex: 1 1 auto; + min-height: 0; + display: flex; + flex-direction: column; + overflow: hidden; +} + +.data-panel-section { + display: none; + flex: 1; + min-height: 0; + overflow: hidden; +} + +.data-panel-section.active { + display: flex; + flex-direction: column; + overflow-y: auto; + overflow-x: hidden; + scrollbar-gutter: stable; +} + +#dataPanelSub2Api.data-panel-section.active { + overflow: hidden; +} + +.pool-section-header { + padding: 8px 10px 5px; + font-size: 10px; + font-weight: 700; + color: var(--text-muted); + letter-spacing: .4px; + text-transform: uppercase; + background: var(--bg-base); + border-bottom: 1px solid var(--border); + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + flex-wrap: wrap; +} + +.pool-section-title { + display: inline-flex; + align-items: center; + gap: 8px; + min-width: 0; +} + +.pool-section-actions { + display: flex; + align-items: center; + justify-content: flex-end; + gap: 6px; + min-width: 0; + flex-wrap: wrap; +} + +.pool-section-actions .inline-status { + flex: 1 1 100%; + text-align: right; + font-size: 11px; + padding: 0; +} + +.pool-overview { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(62px, 1fr)); + gap: 5px; + padding: 8px 10px; + border-bottom: 1px solid var(--separator); + background: var(--bg-surface); + flex-shrink: 0; +} + +.pool-stat-card { + background: var(--bg-card); + border-radius: var(--radius-sm); + border: 1px solid var(--border-card); + padding: 5px 6px; + text-align: center; + min-width: 0; + transition: all .15s var(--ease-spring); +} +.pool-stat-card:hover { border-color: rgba(255,255,255,.1); } +.pool-stat-card .stat-value { font-size: 15px; } +.pool-stat-card .stat-label { font-size: 8px; } + +.remote-panel-wrap .btn-sm { + padding: 5px 9px; + font-size: 11px; +} + +/* ---------- Token List ---------- */ +.pool-table-wrap { + flex: 1; + min-height: 0; + display: flex; + flex-direction: column; + overflow: hidden; +} + +.local-token-wrap { + flex: 1; + min-height: 0; + max-height: none; + background: var(--bg-surface); +} + +.local-token-panel-section.active { + overflow: hidden; +} + +.pool-table-wrap .token-list { padding: 6px 12px; } + +.sub2api-account-wrap { + flex: 1 1 auto; + min-height: 0; + display: flex; + flex-direction: column; + background: var(--bg-surface); + overflow: hidden; +} + +.sub2api-account-wrap .sub2api-account-list { + flex: 1 1 auto; + min-height: 0; + max-height: none; + overflow-y: auto; + overflow-x: hidden; + scrollbar-gutter: stable; + padding: 6px 10px 8px; +} + +.sub2api-account-wrap .token-filter-row, +.sub2api-account-wrap .pool-table-footer { + flex-shrink: 0; +} + +.pool-table-footer { + padding: 7px 10px; + border-top: 1px solid var(--separator); + background: var(--bg-surface); + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + flex-wrap: wrap; +} + +.pool-table-footer .inline-status, +.account-toolbar .inline-status { + flex: 1 1 100%; +} + +.token-filter-row { + padding: 6px 10px; + display: flex; + align-items: center; + gap: 5px; + border-bottom: 1px solid var(--separator); + background: var(--bg-surface); + flex-wrap: wrap; +} + +.account-toolbar { + align-items: center; +} + +.account-select-all { + display: inline-flex; + align-items: center; + gap: 6px; + min-height: 28px; + font-size: 11px; + color: var(--text-secondary); + cursor: pointer; +} + +.account-select-all input { + width: 16px; + height: 16px; + accent-color: var(--accent-blue); + cursor: pointer; +} + +.account-pager { + display: inline-flex; + align-items: center; + gap: 6px; + flex-wrap: wrap; +} + +.pager-info { + font-size: 11px; + color: var(--text-secondary); + min-width: 0; + text-align: center; +} + +.pager-size-select { + min-width: 76px; +} + +.remote-panel-wrap .token-filter-select { + min-width: 86px; + padding: 5px 8px; + font-size: 11px; +} + +.remote-panel-wrap input[type="text"] { + font-size: 11px; + padding: 7px 10px; +} + +.platform-note-wrap { + flex: 1 1 auto; + min-height: 0; + padding: 10px; + overflow: auto; + background: var(--bg-surface); +} + +.platform-note-card { + display: flex; + flex-direction: column; + gap: 8px; + padding: 12px; + border-radius: var(--radius-md); + border: 1px solid var(--border-card); + background: linear-gradient(180deg, rgba(255,255,255,.03) 0%, rgba(255,255,255,.015) 100%); +} + +.platform-note-title { + font-size: 13px; + font-weight: 700; + color: var(--text-primary); +} + +.platform-note-text { + font-size: 12px; + line-height: 1.65; + color: var(--text-secondary); +} + +.token-filter-select { + min-width: 100px; + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: var(--radius-sm); + color: var(--text-primary); + font-size: 12px; + padding: 6px 10px; + outline: none; + cursor: pointer; + font-family: inherit; + transition: border-color var(--duration-fast), box-shadow var(--duration-fast); +} +.token-filter-select:focus { border-color: var(--accent-blue); box-shadow: 0 0 0 3px var(--accent-blue-dim); } +#tokenFilterKeyword { flex: 1; min-width: 120px; } + +.token-list { + flex: 1; + overflow-y: auto; + padding: 8px 12px; +} + +.token-item { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + padding: 10px 12px; + border-radius: var(--radius-md); + background: var(--bg-card); + border: 1px solid var(--border-card); + margin-bottom: 6px; + transition: all .15s var(--ease-spring); + animation: fade-up .25s var(--ease-spring); +} +.token-item:hover { + background: var(--bg-elevated); + border-color: rgba(255,255,255,.1); + transform: translateY(-1px); +} + +.sub2api-account-item { + gap: 8px; + padding: 8px 10px; + margin-bottom: 5px; +} + +.sub2api-account-item.selected { + border-color: rgba(10,132,255,.45); + box-shadow: 0 0 0 1px var(--accent-blue-dim); +} + +.sub2api-account-item .token-email { + font-size: 11px; + gap: 4px; + flex-wrap: wrap; +} + +.sub2api-account-item .token-meta { + font-size: 9px; +} + +.sub2api-account-item .token-actions { + flex-wrap: wrap; + justify-content: flex-end; +} + +.sub2api-account-item .account-status-badge, +.sub2api-account-item .account-flag-badge { + height: 16px; + padding: 0 6px; + font-size: 8px; +} + +.account-check-wrap { + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +.account-check-wrap input { + width: 16px; + height: 16px; + accent-color: var(--accent-blue); + cursor: pointer; +} + +@keyframes fade-up { + from { opacity: 0; transform: translateY(6px); } + to { opacity: 1; transform: translateY(0); } +} + +.token-info { flex: 1; min-width: 0; } +.token-email { + font-size: 12px; + font-weight: 500; + color: var(--text-primary); + display: flex; + align-items: center; + gap: 6px; + min-width: 0; +} +.token-email-text { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; } +.token-meta { font-size: 10px; color: var(--text-muted); font-family: 'JetBrains Mono', monospace; } +.token-actions { display: flex; gap: 4px; flex-shrink: 0; } + +.account-status-badge, +.account-flag-badge { + display: inline-flex; + align-items: center; + height: 18px; + padding: 0 7px; + border-radius: var(--radius-xs); + font-size: 9px; + font-weight: 700; + letter-spacing: .3px; + white-space: nowrap; +} + +.account-status-badge.ok { + color: var(--accent-green); + background: var(--accent-green-dim); +} + +.account-status-badge.warn { + color: var(--accent-orange); + background: var(--accent-orange-dim); +} + +.account-status-badge.danger { + color: var(--accent-red); + background: var(--accent-red-dim); +} + +.account-flag-badge.duplicate { + color: var(--accent-blue); + background: var(--accent-blue-dim); +} + +.account-flag-badge.keep { + color: var(--accent-green); + background: var(--accent-green-dim); +} + +.account-flag-badge.delete { + color: var(--accent-orange); + background: var(--accent-orange-dim); +} + +.synced-badge { + font-size: 9px; + font-weight: 700; + color: var(--accent-green); + background: var(--accent-green-dim); + padding: 2px 7px; + border-radius: var(--radius-pill); + white-space: nowrap; + flex-shrink: 0; +} + +.token-platforms { display: flex; align-items: center; gap: 4px; flex-wrap: wrap; margin: 3px 0 2px; } + +.platform-badge { + display: inline-flex; + align-items: center; + height: 18px; + padding: 0 7px; + border-radius: var(--radius-xs); + font-size: 9px; + font-weight: 700; + letter-spacing: .3px; + white-space: nowrap; +} +.platform-badge.cpa { color: var(--accent-orange); background: var(--accent-orange-dim); } +.platform-badge.sub2api { color: var(--accent-green); background: var(--accent-green-dim); } +.platform-badge.none { color: var(--text-muted); background: rgba(255,255,255,.04); } + +.empty-state { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100%; + color: var(--text-muted); + gap: 10px; + font-size: 13px; + padding: 30px; +} +.empty-icon { opacity: .2; font-size: 32px; } + +/* ========================================== + TOAST NOTIFICATIONS — Refined + ========================================== */ +.toast-container { + position: fixed; + top: 64px; + right: 16px; + z-index: 1000; + display: flex; + flex-direction: column; + gap: 8px; + pointer-events: none; +} + +.toast { + background: var(--bg-elevated); + border: 1px solid var(--border-card); + border-radius: var(--radius-md); + padding: 10px 16px; + font-size: 13px; + font-weight: 500; + animation: toast-slide .3s var(--ease-spring); + max-width: 360px; + backdrop-filter: blur(20px); + -webkit-backdrop-filter: blur(20px); + pointer-events: auto; + display: flex; + align-items: center; + gap: 8px; + box-shadow: var(--shadow-elevated); +} + +.toast-icon { + flex-shrink: 0; + width: 18px; + height: 18px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 11px; + font-weight: 800; +} + +@keyframes toast-slide { + from { opacity: 0; transform: translateX(16px) scale(.95); } + to { opacity: 1; transform: translateX(0) scale(1); } +} + +@keyframes toast-out { + from { opacity: 1; transform: translateX(0) scale(1); } + to { opacity: 0; transform: translateX(16px) scale(.95); } +} + +.toast.success { color: var(--accent-green); border-color: rgba(48,209,88,.2); } +.toast.success .toast-icon { background: var(--accent-green-dim); color: var(--accent-green); } +.toast.error { color: var(--accent-red); border-color: rgba(255,69,58,.2); } +.toast.error .toast-icon { background: var(--accent-red-dim); color: var(--accent-red); } +.toast.info { color: var(--accent-blue); border-color: rgba(10,132,255,.2); } +.toast.info .toast-icon { background: var(--accent-blue-dim); color: var(--accent-blue); } + +/* ========================================== + CONFIG PAGE + ========================================== */ +.config-page { + flex: 1; + min-height: 0; + overflow-y: auto; + padding: 20px; + display: flex; + flex-wrap: wrap; + gap: 16px; + align-content: start; + background: var(--bg-base); +} + +.config-card { + flex: 1 1 calc(50% - 8px); + min-width: 420px; + max-width: 100%; + background: var(--bg-surface); + border-radius: var(--radius-lg); + overflow: visible; + border: 1px solid var(--border-card); + transition: border-color .2s var(--ease-spring); +} +.config-card:hover { border-color: rgba(255,255,255,.1); } + +.config-page .config-card[style*="grid-column: span 2"], +.config-page .config-card[style*="span 2"] { + flex-basis: 100% !important; + min-width: 100%; +} + +.collapsible-trigger { + display: flex; + align-items: center; + justify-content: space-between; + padding: 14px 16px; + font-size: 14px; + font-weight: 700; + color: var(--text-primary); + border-bottom: 1px solid var(--separator); + cursor: default; + pointer-events: none; + background: rgba(255,255,255,.02); +} + +.collapse-icon { display: none; } + +.collapsible-body { + padding: 16px; + min-width: 0; +} + +/* Config page: always show body */ +.config-page .collapsible-body { display: block !important; } + +.config-field { margin-bottom: 12px; } +.config-field label { + display: block; + font-size: 11px; + font-weight: 600; + color: var(--text-secondary); + margin-bottom: 5px; + letter-spacing: .3px; +} + +.config-field select { + width: 100%; + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: var(--radius-sm); + color: var(--text-primary); + font-size: 12px; + padding: 9px 12px; + outline: none; + cursor: pointer; + font-family: inherit; + -webkit-appearance: none; + transition: border-color var(--duration-fast), box-shadow var(--duration-fast); +} +.config-field select:focus { border-color: var(--accent-blue); box-shadow: 0 0 0 3px var(--accent-blue-dim); } + +.config-field input[type="number"] { + width: 100%; + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: var(--radius-sm); + color: var(--text-primary); + font-family: 'JetBrains Mono', monospace; + font-size: 12px; + padding: 9px 12px; + outline: none; + transition: border-color var(--duration-fast), box-shadow var(--duration-fast); +} +.config-field input[type="number"]:focus { border-color: var(--accent-blue); box-shadow: 0 0 0 3px var(--accent-blue-dim); } + +.config-hint { + font-size: 11px; + color: var(--text-muted); + margin-top: 6px; + line-height: 1.6; + padding: 8px 10px; + background: rgba(255,255,255,.02); + border-radius: var(--radius-sm); + border-left: 2px solid var(--accent-blue-dim); +} + +.config-row { display: flex; gap: 12px; align-items: flex-end; flex-wrap: wrap; } +.config-row .config-field { min-width: 180px; } +.config-actions { display: flex; gap: 8px; margin-top: 14px; align-items: center; flex-wrap: wrap; } + +.maintain-option-grid { + display: flex; + flex-direction: column; + gap: 8px; + margin-top: 10px; +} + +.maintain-option { + display: inline-flex; + align-items: center; + gap: 8px; + min-height: 36px; + padding: 10px 12px; + border: 1px solid var(--border-card); + border-radius: var(--radius-sm); + background: var(--bg-card); + color: var(--text-secondary); + cursor: pointer; +} + +.maintain-option input { + width: 16px; + height: 16px; + accent-color: var(--accent-blue); + cursor: pointer; +} + +.inline-status { + font-size: 12px; + color: var(--text-secondary); + padding: 4px 0; + min-height: 20px; +} + +.inline-status:empty { + display: none; +} + +.config-status { + font-size: 12px; + margin-top: 8px; + padding: 6px 10px; + border-radius: var(--radius-sm); + background: var(--bg-card); + color: var(--text-muted); + min-height: 26px; + border: 1px solid var(--border-card); +} + +/* ========================================== + MAIL PROVIDERS — iOS Grouped Style + ========================================== */ +.mail-providers-group { display: flex; flex-direction: column; gap: 6px; } + +.provider-item { + background: var(--bg-card); + border-radius: var(--radius-md); + overflow: hidden; + border: 1px solid var(--border-card); + transition: border-color .2s; +} +.provider-item:has(.mail-provider-check:checked) { border-color: rgba(10,132,255,.2); } + +.provider-toggle { + display: flex; + align-items: center; + gap: 10px; + padding: 10px 12px; + cursor: pointer; + font-size: 13px; + font-weight: 500; + color: var(--text-primary); + user-select: none; +} + +.provider-toggle input[type="checkbox"] { + position: absolute; + opacity: 0; + width: 0; + height: 0; +} + +.provider-check-mark { + width: 20px; + height: 20px; + border-radius: 6px; + border: 2px solid rgba(255,255,255,.18); + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + transition: all .2s var(--ease-spring); + position: relative; +} + +.provider-check-mark::after { + content: ''; + width: 5px; + height: 9px; + border: solid #fff; + border-width: 0 2px 2px 0; + transform: rotate(45deg) scale(0); + transition: transform .2s var(--ease-bounce); + position: absolute; + top: 2px; + left: 5px; +} + +.provider-toggle input:checked ~ .provider-check-mark { + background: var(--accent-blue); + border-color: var(--accent-blue); +} + +.provider-toggle input:checked ~ .provider-check-mark::after { + transform: rotate(45deg) scale(1); +} + +.provider-config { + padding: 4px 12px 12px; + border-top: 1px solid var(--separator); +} +.provider-config .config-field { margin-top: 6px; } + +/* ========================================== + RESPONSIVE + ========================================== */ +@media (max-width: 1280px) { + .app-shell { --col-right: 280px; } + .pool-chip { min-width: 160px; } + .pool-stat-card { padding: 4px 6px; } + .pool-stat-card .stat-value { font-size: 15px; } + .config-card { min-width: 360px; } + .pool-chip { min-width: 136px; } +} + +@media (max-width: 1100px) { + .theme-toggle-label { display: none; } + .theme-toggle-btn { min-width: 36px; padding: 0 8px; } +} + +@media (max-width: 960px) { + header { padding: 0 12px; } + .header-tab-nav { padding: 0; } + .header-right { gap: 6px; } + .pool-chip { min-width: 124px; height: 42px; grid-template-rows: 12px 14px 3px; padding: 5px 8px 4px; } + .pool-chip-name { font-size: 9px; } + .pool-chip-value { font-size: 11px; } + .pool-chip-delta { font-size: 10px; padding: 2px 5px; } + .theme-toggle-btn { min-width: 34px; height: 30px; padding: 0 8px; font-size: 11px; gap: 6px; } + .token-filter-row { padding: 6px 10px; } + .header-tab-nav .segmented-control { width: auto; } + .header-tab-nav .tab-btn { min-width: 0; padding: 6px 14px; } +} + +@media (max-width: 900px) { + .config-page { padding: 12px; } + .config-card { flex: 1 1 100%; min-width: 100%; } + .config-row .config-field { min-width: 0; flex: 1 1 100% !important; } +} + +body.theme-light header { + background: rgba(255,255,255,.88); +} + +body.theme-light .header-brand .logo { + background: linear-gradient(135deg, #111111 0%, #555555 100%); + box-shadow: none; +} + +body.theme-light .segmented-control { + background: rgba(17,17,17,.10); +} + +body.theme-light .segment-indicator { + box-shadow: 0 1px 3px rgba(0,0,0,.08), 0 0 0 .5px rgba(0,0,0,.12); +} + +body.theme-light .theme-toggle-btn:hover { + border-color: rgba(0,0,0,.18); +} + +body.theme-light * { + scrollbar-color: rgba(0,0,0,.24) transparent; +} + +body.theme-light ::-webkit-scrollbar-thumb { + background: rgba(0,0,0,.18); +} + +body.theme-light ::-webkit-scrollbar-thumb:hover { + background: rgba(0,0,0,.28); +} + +/* Helpers */ +.token-header .btn, .stats-grid, .stat-card, .control-buttons, .control-buttons .btn, +.proxy-row, .log-header, .log-entry, .log-msg { min-width: 0; } + +/* Legacy compat — collapsible behavior kept for sidebar */ +.config-section { padding: 16px; border-bottom: none; } +.collapsible .collapsible-body { display: none; } +.collapsible.open .collapsible-body { display: block; } +.collapsible.open .collapse-icon { transform: rotate(90deg); } +.config-conditional { border-top: 1px dashed var(--separator); padding-top: 12px; margin-top: 4px; } + +/* Token panel (legacy compat) */ +.token-panel { display: flex; flex-direction: column; overflow: hidden; background: var(--bg-surface); border-left: 1px solid var(--border); min-width: 0; } +.token-header { padding: 12px 16px; display: flex; align-items: center; justify-content: space-between; border-bottom: 1px solid var(--border); flex-shrink: 0; flex-wrap: wrap; gap: 8px; } + +/* Multithread row (legacy compat) */ +.multithread-row { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 10px; +} +.multithread-row .toggle-label { + display: flex; + align-items: center; + gap: 6px; + font-size: 13px; + cursor: pointer; + color: var(--text-secondary); +} + +/* ========================================== + SELECTION & FOCUS — Global + ========================================== */ +::selection { + background: rgba(10,132,255,.3); + color: #fff; +} + +/* Focus visible — accessibility ring */ +:focus-visible { + outline: 2px solid var(--accent-blue); + outline-offset: 2px; +} + +button:focus-visible { + outline: 2px solid var(--accent-blue); + outline-offset: 2px; +} + diff --git a/pyproject.toml b/pyproject.toml new file mode 100755 index 0000000..e26a46a --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,38 @@ +[project] +name = "openai-pool-orchestrator" +version = "2.0.0" +description = "OpenAI 账号池编排器 — 自动化注册、Token 管理与多平台账号池维护" +readme = "README.md" +license = {text = "MIT"} +requires-python = ">=3.10" +keywords = ["openai", "account-pool", "automation", "token-management"] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Topic :: Software Development :: Libraries", +] +dependencies = [ + "fastapi>=0.110", + "uvicorn[standard]>=0.27", + "curl-cffi>=0.6", + "aiohttp>=3.9", + "requests>=2.31", +] + +[project.scripts] +openai-pool = "openai_pool_orchestrator.__main__:main" + +[build-system] +requires = ["setuptools>=68.0", "wheel"] +build-backend = "setuptools.build_meta" + +[tool.setuptools.packages.find] +include = ["openai_pool_orchestrator*"] + +[tool.setuptools.package-data] +openai_pool_orchestrator = ["static/**/*"] diff --git a/requirements.txt b/requirements.txt new file mode 100755 index 0000000..28a8fcc --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +fastapi>=0.110 +uvicorn[standard]>=0.27 +curl-cffi>=0.6 +aiohttp>=3.9 +requests>=2.31 diff --git a/run.py b/run.py new file mode 100755 index 0000000..e12c25d --- /dev/null +++ b/run.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python3 +""" +快速启动脚本 - OpenAI Pool Orchestrator + +用法: + python run.py # 启动 Web 服务 + python run.py --cli # CLI 模式(单次注册) + python run.py --cli --proxy http://127.0.0.1:7897 +""" + +import sys +import os + +# 将项目根目录加入 Python 路径 +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + + +def main(): + if "--cli" in sys.argv: + # CLI 模式:直接调用注册脚本 + sys.argv.remove("--cli") + from openai_pool_orchestrator.register import main as cli_main + cli_main() + else: + # Web 模式:启动 FastAPI 服务 + from openai_pool_orchestrator.__main__ import main as web_main + web_main() + + +if __name__ == "__main__": + main() diff --git a/uv.lock b/uv.lock new file mode 100755 index 0000000..0914e8f --- /dev/null +++ b/uv.lock @@ -0,0 +1,1555 @@ +version = 1 +revision = 1 +requires-python = ">=3.10" + +[[package]] +name = "aiohappyeyeballs" +version = "2.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/26/30/f84a107a9c4331c14b2b586036f40965c128aa4fee4dda5d3d51cb14ad54/aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", size = 22760 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265 }, +] + +[[package]] +name = "aiohttp" +version = "3.13.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohappyeyeballs" }, + { name = "aiosignal" }, + { name = "async-timeout", marker = "python_full_version < '3.11'" }, + { name = "attrs" }, + { name = "frozenlist" }, + { name = "multidict" }, + { name = "propcache" }, + { name = "yarl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/42/32cf8e7704ceb4481406eb87161349abb46a57fee3f008ba9cb610968646/aiohttp-3.13.3.tar.gz", hash = "sha256:a949eee43d3782f2daae4f4a2819b2cb9b0c5d3b7f7a927067cc84dafdbb9f88", size = 7844556 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/d6/5aec9313ee6ea9c7cde8b891b69f4ff4001416867104580670a31daeba5b/aiohttp-3.13.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d5a372fd5afd301b3a89582817fdcdb6c34124787c70dbcc616f259013e7eef7", size = 738950 }, + { url = "https://files.pythonhosted.org/packages/68/03/8fa90a7e6d11ff20a18837a8e2b5dd23db01aabc475aa9271c8ad33299f5/aiohttp-3.13.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:147e422fd1223005c22b4fe080f5d93ced44460f5f9c105406b753612b587821", size = 496099 }, + { url = "https://files.pythonhosted.org/packages/d2/23/b81f744d402510a8366b74eb420fc0cc1170d0c43daca12d10814df85f10/aiohttp-3.13.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:859bd3f2156e81dd01432f5849fc73e2243d4a487c4fd26609b1299534ee1845", size = 491072 }, + { url = "https://files.pythonhosted.org/packages/d5/e1/56d1d1c0dd334cd203dd97706ce004c1aa24b34a813b0b8daf3383039706/aiohttp-3.13.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dca68018bf48c251ba17c72ed479f4dafe9dbd5a73707ad8d28a38d11f3d42af", size = 1671588 }, + { url = "https://files.pythonhosted.org/packages/5f/34/8d7f962604f4bc2b4e39eb1220dac7d4e4cba91fb9ba0474b4ecd67db165/aiohttp-3.13.3-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fee0c6bc7db1de362252affec009707a17478a00ec69f797d23ca256e36d5940", size = 1640334 }, + { url = "https://files.pythonhosted.org/packages/94/1d/fcccf2c668d87337ddeef9881537baee13c58d8f01f12ba8a24215f2b804/aiohttp-3.13.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c048058117fd649334d81b4b526e94bde3ccaddb20463a815ced6ecbb7d11160", size = 1722656 }, + { url = "https://files.pythonhosted.org/packages/aa/98/c6f3b081c4c606bc1e5f2ec102e87d6411c73a9ef3616fea6f2d5c98c062/aiohttp-3.13.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:215a685b6fbbfcf71dfe96e3eba7a6f58f10da1dfdf4889c7dd856abe430dca7", size = 1817625 }, + { url = "https://files.pythonhosted.org/packages/2c/c0/cfcc3d2e11b477f86e1af2863f3858c8850d751ce8dc39c4058a072c9e54/aiohttp-3.13.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2c184bb1fe2cbd2cefba613e9db29a5ab559323f994b6737e370d3da0ac455", size = 1672604 }, + { url = "https://files.pythonhosted.org/packages/1e/77/6b4ffcbcac4c6a5d041343a756f34a6dd26174ae07f977a64fe028dda5b0/aiohttp-3.13.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:75ca857eba4e20ce9f546cd59c7007b33906a4cd48f2ff6ccf1ccfc3b646f279", size = 1554370 }, + { url = "https://files.pythonhosted.org/packages/f2/f0/e3ddfa93f17d689dbe014ba048f18e0c9f9b456033b70e94349a2e9048be/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:81e97251d9298386c2b7dbeb490d3d1badbdc69107fb8c9299dd04eb39bddc0e", size = 1642023 }, + { url = "https://files.pythonhosted.org/packages/eb/45/c14019c9ec60a8e243d06d601b33dcc4fd92379424bde3021725859d7f99/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:c0e2d366af265797506f0283487223146af57815b388623f0357ef7eac9b209d", size = 1649680 }, + { url = "https://files.pythonhosted.org/packages/9c/fd/09c9451dae5aa5c5ed756df95ff9ef549d45d4be663bafd1e4954fd836f0/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4e239d501f73d6db1522599e14b9b321a7e3b1de66ce33d53a765d975e9f4808", size = 1692407 }, + { url = "https://files.pythonhosted.org/packages/a6/81/938bc2ec33c10efd6637ccb3d22f9f3160d08e8f3aa2587a2c2d5ab578eb/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:0db318f7a6f065d84cb1e02662c526294450b314a02bd9e2a8e67f0d8564ce40", size = 1543047 }, + { url = "https://files.pythonhosted.org/packages/f7/23/80488ee21c8d567c83045e412e1d9b7077d27171591a4eb7822586e8c06a/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:bfc1cc2fe31a6026a8a88e4ecfb98d7f6b1fec150cfd708adbfd1d2f42257c29", size = 1715264 }, + { url = "https://files.pythonhosted.org/packages/e2/83/259a8da6683182768200b368120ab3deff5370bed93880fb9a3a86299f34/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:af71fff7bac6bb7508956696dce8f6eec2bbb045eceb40343944b1ae62b5ef11", size = 1657275 }, + { url = "https://files.pythonhosted.org/packages/3f/4f/2c41f800a0b560785c10fb316216ac058c105f9be50bdc6a285de88db625/aiohttp-3.13.3-cp310-cp310-win32.whl", hash = "sha256:37da61e244d1749798c151421602884db5270faf479cf0ef03af0ff68954c9dd", size = 434053 }, + { url = "https://files.pythonhosted.org/packages/80/df/29cd63c7ecfdb65ccc12f7d808cac4fa2a19544660c06c61a4a48462de0c/aiohttp-3.13.3-cp310-cp310-win_amd64.whl", hash = "sha256:7e63f210bc1b57ef699035f2b4b6d9ce096b5914414a49b0997c839b2bd2223c", size = 456687 }, + { url = "https://files.pythonhosted.org/packages/f1/4c/a164164834f03924d9a29dc3acd9e7ee58f95857e0b467f6d04298594ebb/aiohttp-3.13.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5b6073099fb654e0a068ae678b10feff95c5cae95bbfcbfa7af669d361a8aa6b", size = 746051 }, + { url = "https://files.pythonhosted.org/packages/82/71/d5c31390d18d4f58115037c432b7e0348c60f6f53b727cad33172144a112/aiohttp-3.13.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cb93e166e6c28716c8c6aeb5f99dfb6d5ccf482d29fe9bf9a794110e6d0ab64", size = 499234 }, + { url = "https://files.pythonhosted.org/packages/0e/c9/741f8ac91e14b1d2e7100690425a5b2b919a87a5075406582991fb7de920/aiohttp-3.13.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:28e027cf2f6b641693a09f631759b4d9ce9165099d2b5d92af9bd4e197690eea", size = 494979 }, + { url = "https://files.pythonhosted.org/packages/75/b5/31d4d2e802dfd59f74ed47eba48869c1c21552c586d5e81a9d0d5c2ad640/aiohttp-3.13.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3b61b7169ababd7802f9568ed96142616a9118dd2be0d1866e920e77ec8fa92a", size = 1748297 }, + { url = "https://files.pythonhosted.org/packages/1a/3e/eefad0ad42959f226bb79664826883f2687d602a9ae2941a18e0484a74d3/aiohttp-3.13.3-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:80dd4c21b0f6237676449c6baaa1039abae86b91636b6c91a7f8e61c87f89540", size = 1707172 }, + { url = "https://files.pythonhosted.org/packages/c5/3a/54a64299fac2891c346cdcf2aa6803f994a2e4beeaf2e5a09dcc54acc842/aiohttp-3.13.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:65d2ccb7eabee90ce0503c17716fc77226be026dcc3e65cce859a30db715025b", size = 1805405 }, + { url = "https://files.pythonhosted.org/packages/6c/70/ddc1b7169cf64075e864f64595a14b147a895a868394a48f6a8031979038/aiohttp-3.13.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5b179331a481cb5529fca8b432d8d3c7001cb217513c94cd72d668d1248688a3", size = 1899449 }, + { url = "https://files.pythonhosted.org/packages/a1/7e/6815aab7d3a56610891c76ef79095677b8b5be6646aaf00f69b221765021/aiohttp-3.13.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d4c940f02f49483b18b079d1c27ab948721852b281f8b015c058100e9421dd1", size = 1748444 }, + { url = "https://files.pythonhosted.org/packages/6b/f2/073b145c4100da5511f457dc0f7558e99b2987cf72600d42b559db856fbc/aiohttp-3.13.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f9444f105664c4ce47a2a7171a2418bce5b7bae45fb610f4e2c36045d85911d3", size = 1606038 }, + { url = "https://files.pythonhosted.org/packages/0a/c1/778d011920cae03ae01424ec202c513dc69243cf2db303965615b81deeea/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:694976222c711d1d00ba131904beb60534f93966562f64440d0c9d41b8cdb440", size = 1724156 }, + { url = "https://files.pythonhosted.org/packages/0e/cb/3419eabf4ec1e9ec6f242c32b689248365a1cf621891f6f0386632525494/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f33ed1a2bf1997a36661874b017f5c4b760f41266341af36febaf271d179f6d7", size = 1722340 }, + { url = "https://files.pythonhosted.org/packages/7a/e5/76cf77bdbc435bf233c1f114edad39ed4177ccbfab7c329482b179cff4f4/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e636b3c5f61da31a92bf0d91da83e58fdfa96f178ba682f11d24f31944cdd28c", size = 1783041 }, + { url = "https://files.pythonhosted.org/packages/9d/d4/dd1ca234c794fd29c057ce8c0566b8ef7fd6a51069de5f06fa84b9a1971c/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:5d2d94f1f5fcbe40838ac51a6ab5704a6f9ea42e72ceda48de5e6b898521da51", size = 1596024 }, + { url = "https://files.pythonhosted.org/packages/55/58/4345b5f26661a6180afa686c473620c30a66afdf120ed3dd545bbc809e85/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2be0e9ccf23e8a94f6f0650ce06042cefc6ac703d0d7ab6c7a917289f2539ad4", size = 1804590 }, + { url = "https://files.pythonhosted.org/packages/7b/06/05950619af6c2df7e0a431d889ba2813c9f0129cec76f663e547a5ad56f2/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9af5e68ee47d6534d36791bbe9b646d2a7c7deb6fc24d7943628edfbb3581f29", size = 1740355 }, + { url = "https://files.pythonhosted.org/packages/3e/80/958f16de79ba0422d7c1e284b2abd0c84bc03394fbe631d0a39ffa10e1eb/aiohttp-3.13.3-cp311-cp311-win32.whl", hash = "sha256:a2212ad43c0833a873d0fb3c63fa1bacedd4cf6af2fee62bf4b739ceec3ab239", size = 433701 }, + { url = "https://files.pythonhosted.org/packages/dc/f2/27cdf04c9851712d6c1b99df6821a6623c3c9e55956d4b1e318c337b5a48/aiohttp-3.13.3-cp311-cp311-win_amd64.whl", hash = "sha256:642f752c3eb117b105acbd87e2c143de710987e09860d674e068c4c2c441034f", size = 457678 }, + { url = "https://files.pythonhosted.org/packages/a0/be/4fc11f202955a69e0db803a12a062b8379c970c7c84f4882b6da17337cc1/aiohttp-3.13.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b903a4dfee7d347e2d87697d0713be59e0b87925be030c9178c5faa58ea58d5c", size = 739732 }, + { url = "https://files.pythonhosted.org/packages/97/2c/621d5b851f94fa0bb7430d6089b3aa970a9d9b75196bc93bb624b0db237a/aiohttp-3.13.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a45530014d7a1e09f4a55f4f43097ba0fd155089372e105e4bff4ca76cb1b168", size = 494293 }, + { url = "https://files.pythonhosted.org/packages/5d/43/4be01406b78e1be8320bb8316dc9c42dbab553d281c40364e0f862d5661c/aiohttp-3.13.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:27234ef6d85c914f9efeb77ff616dbf4ad2380be0cda40b4db086ffc7ddd1b7d", size = 493533 }, + { url = "https://files.pythonhosted.org/packages/8d/a8/5a35dc56a06a2c90d4742cbf35294396907027f80eea696637945a106f25/aiohttp-3.13.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d32764c6c9aafb7fb55366a224756387cd50bfa720f32b88e0e6fa45b27dcf29", size = 1737839 }, + { url = "https://files.pythonhosted.org/packages/bf/62/4b9eeb331da56530bf2e198a297e5303e1c1ebdceeb00fe9b568a65c5a0c/aiohttp-3.13.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b1a6102b4d3ebc07dad44fbf07b45bb600300f15b552ddf1851b5390202ea2e3", size = 1703932 }, + { url = "https://files.pythonhosted.org/packages/7c/f6/af16887b5d419e6a367095994c0b1332d154f647e7dc2bd50e61876e8e3d/aiohttp-3.13.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c014c7ea7fb775dd015b2d3137378b7be0249a448a1612268b5a90c2d81de04d", size = 1771906 }, + { url = "https://files.pythonhosted.org/packages/ce/83/397c634b1bcc24292fa1e0c7822800f9f6569e32934bdeef09dae7992dfb/aiohttp-3.13.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2b8d8ddba8f95ba17582226f80e2de99c7a7948e66490ef8d947e272a93e9463", size = 1871020 }, + { url = "https://files.pythonhosted.org/packages/86/f6/a62cbbf13f0ac80a70f71b1672feba90fdb21fd7abd8dbf25c0105fb6fa3/aiohttp-3.13.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ae8dd55c8e6c4257eae3a20fd2c8f41edaea5992ed67156642493b8daf3cecc", size = 1755181 }, + { url = "https://files.pythonhosted.org/packages/0a/87/20a35ad487efdd3fba93d5843efdfaa62d2f1479eaafa7453398a44faf13/aiohttp-3.13.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:01ad2529d4b5035578f5081606a465f3b814c542882804e2e8cda61adf5c71bf", size = 1561794 }, + { url = "https://files.pythonhosted.org/packages/de/95/8fd69a66682012f6716e1bc09ef8a1a2a91922c5725cb904689f112309c4/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bb4f7475e359992b580559e008c598091c45b5088f28614e855e42d39c2f1033", size = 1697900 }, + { url = "https://files.pythonhosted.org/packages/e5/66/7b94b3b5ba70e955ff597672dad1691333080e37f50280178967aff68657/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:c19b90316ad3b24c69cd78d5c9b4f3aa4497643685901185b65166293d36a00f", size = 1728239 }, + { url = "https://files.pythonhosted.org/packages/47/71/6f72f77f9f7d74719692ab65a2a0252584bf8d5f301e2ecb4c0da734530a/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:96d604498a7c782cb15a51c406acaea70d8c027ee6b90c569baa6e7b93073679", size = 1740527 }, + { url = "https://files.pythonhosted.org/packages/fa/b4/75ec16cbbd5c01bdaf4a05b19e103e78d7ce1ef7c80867eb0ace42ff4488/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:084911a532763e9d3dd95adf78a78f4096cd5f58cdc18e6fdbc1b58417a45423", size = 1554489 }, + { url = "https://files.pythonhosted.org/packages/52/8f/bc518c0eea29f8406dcf7ed1f96c9b48e3bc3995a96159b3fc11f9e08321/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:7a4a94eb787e606d0a09404b9c38c113d3b099d508021faa615d70a0131907ce", size = 1767852 }, + { url = "https://files.pythonhosted.org/packages/9d/f2/a07a75173124f31f11ea6f863dc44e6f09afe2bca45dd4e64979490deab1/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:87797e645d9d8e222e04160ee32aa06bc5c163e8499f24db719e7852ec23093a", size = 1722379 }, + { url = "https://files.pythonhosted.org/packages/3c/4a/1a3fee7c21350cac78e5c5cef711bac1b94feca07399f3d406972e2d8fcd/aiohttp-3.13.3-cp312-cp312-win32.whl", hash = "sha256:b04be762396457bef43f3597c991e192ee7da460a4953d7e647ee4b1c28e7046", size = 428253 }, + { url = "https://files.pythonhosted.org/packages/d9/b7/76175c7cb4eb73d91ad63c34e29fc4f77c9386bba4a65b53ba8e05ee3c39/aiohttp-3.13.3-cp312-cp312-win_amd64.whl", hash = "sha256:e3531d63d3bdfa7e3ac5e9b27b2dd7ec9df3206a98e0b3445fa906f233264c57", size = 455407 }, + { url = "https://files.pythonhosted.org/packages/97/8a/12ca489246ca1faaf5432844adbfce7ff2cc4997733e0af120869345643a/aiohttp-3.13.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5dff64413671b0d3e7d5918ea490bdccb97a4ad29b3f311ed423200b2203e01c", size = 734190 }, + { url = "https://files.pythonhosted.org/packages/32/08/de43984c74ed1fca5c014808963cc83cb00d7bb06af228f132d33862ca76/aiohttp-3.13.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:87b9aab6d6ed88235aa2970294f496ff1a1f9adcd724d800e9b952395a80ffd9", size = 491783 }, + { url = "https://files.pythonhosted.org/packages/17/f8/8dd2cf6112a5a76f81f81a5130c57ca829d101ad583ce57f889179accdda/aiohttp-3.13.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:425c126c0dc43861e22cb1c14ba4c8e45d09516d0a3ae0a3f7494b79f5f233a3", size = 490704 }, + { url = "https://files.pythonhosted.org/packages/6d/40/a46b03ca03936f832bc7eaa47cfbb1ad012ba1be4790122ee4f4f8cba074/aiohttp-3.13.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f9120f7093c2a32d9647abcaf21e6ad275b4fbec5b55969f978b1a97c7c86bf", size = 1720652 }, + { url = "https://files.pythonhosted.org/packages/f7/7e/917fe18e3607af92657e4285498f500dca797ff8c918bd7d90b05abf6c2a/aiohttp-3.13.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:697753042d57f4bf7122cab985bf15d0cef23c770864580f5af4f52023a56bd6", size = 1692014 }, + { url = "https://files.pythonhosted.org/packages/71/b6/cefa4cbc00d315d68973b671cf105b21a609c12b82d52e5d0c9ae61d2a09/aiohttp-3.13.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6de499a1a44e7de70735d0b39f67c8f25eb3d91eb3103be99ca0fa882cdd987d", size = 1759777 }, + { url = "https://files.pythonhosted.org/packages/fb/e3/e06ee07b45e59e6d81498b591fc589629be1553abb2a82ce33efe2a7b068/aiohttp-3.13.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:37239e9f9a7ea9ac5bf6b92b0260b01f8a22281996da609206a84df860bc1261", size = 1861276 }, + { url = "https://files.pythonhosted.org/packages/7c/24/75d274228acf35ceeb2850b8ce04de9dd7355ff7a0b49d607ee60c29c518/aiohttp-3.13.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f76c1e3fe7d7c8afad7ed193f89a292e1999608170dcc9751a7462a87dfd5bc0", size = 1743131 }, + { url = "https://files.pythonhosted.org/packages/04/98/3d21dde21889b17ca2eea54fdcff21b27b93f45b7bb94ca029c31ab59dc3/aiohttp-3.13.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fc290605db2a917f6e81b0e1e0796469871f5af381ce15c604a3c5c7e51cb730", size = 1556863 }, + { url = "https://files.pythonhosted.org/packages/9e/84/da0c3ab1192eaf64782b03971ab4055b475d0db07b17eff925e8c93b3aa5/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4021b51936308aeea0367b8f006dc999ca02bc118a0cc78c303f50a2ff6afb91", size = 1682793 }, + { url = "https://files.pythonhosted.org/packages/ff/0f/5802ada182f575afa02cbd0ec5180d7e13a402afb7c2c03a9aa5e5d49060/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:49a03727c1bba9a97d3e93c9f93ca03a57300f484b6e935463099841261195d3", size = 1716676 }, + { url = "https://files.pythonhosted.org/packages/3f/8c/714d53bd8b5a4560667f7bbbb06b20c2382f9c7847d198370ec6526af39c/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3d9908a48eb7416dc1f4524e69f1d32e5d90e3981e4e37eb0aa1cd18f9cfa2a4", size = 1733217 }, + { url = "https://files.pythonhosted.org/packages/7d/79/e2176f46d2e963facea939f5be2d26368ce543622be6f00a12844d3c991f/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2712039939ec963c237286113c68dbad80a82a4281543f3abf766d9d73228998", size = 1552303 }, + { url = "https://files.pythonhosted.org/packages/ab/6a/28ed4dea1759916090587d1fe57087b03e6c784a642b85ef48217b0277ae/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:7bfdc049127717581866fa4708791220970ce291c23e28ccf3922c700740fdc0", size = 1763673 }, + { url = "https://files.pythonhosted.org/packages/e8/35/4a3daeb8b9fab49240d21c04d50732313295e4bd813a465d840236dd0ce1/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8057c98e0c8472d8846b9c79f56766bcc57e3e8ac7bfd510482332366c56c591", size = 1721120 }, + { url = "https://files.pythonhosted.org/packages/bc/9f/d643bb3c5fb99547323e635e251c609fbbc660d983144cfebec529e09264/aiohttp-3.13.3-cp313-cp313-win32.whl", hash = "sha256:1449ceddcdbcf2e0446957863af03ebaaa03f94c090f945411b61269e2cb5daf", size = 427383 }, + { url = "https://files.pythonhosted.org/packages/4e/f1/ab0395f8a79933577cdd996dd2f9aa6014af9535f65dddcf88204682fe62/aiohttp-3.13.3-cp313-cp313-win_amd64.whl", hash = "sha256:693781c45a4033d31d4187d2436f5ac701e7bbfe5df40d917736108c1cc7436e", size = 453899 }, + { url = "https://files.pythonhosted.org/packages/99/36/5b6514a9f5d66f4e2597e40dea2e3db271e023eb7a5d22defe96ba560996/aiohttp-3.13.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:ea37047c6b367fd4bd632bff8077449b8fa034b69e812a18e0132a00fae6e808", size = 737238 }, + { url = "https://files.pythonhosted.org/packages/f7/49/459327f0d5bcd8c6c9ca69e60fdeebc3622861e696490d8674a6d0cb90a6/aiohttp-3.13.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6fc0e2337d1a4c3e6acafda6a78a39d4c14caea625124817420abceed36e2415", size = 492292 }, + { url = "https://files.pythonhosted.org/packages/e8/0b/b97660c5fd05d3495b4eb27f2d0ef18dc1dc4eff7511a9bf371397ff0264/aiohttp-3.13.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c685f2d80bb67ca8c3837823ad76196b3694b0159d232206d1e461d3d434666f", size = 493021 }, + { url = "https://files.pythonhosted.org/packages/54/d4/438efabdf74e30aeceb890c3290bbaa449780583b1270b00661126b8aae4/aiohttp-3.13.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48e377758516d262bde50c2584fc6c578af272559c409eecbdd2bae1601184d6", size = 1717263 }, + { url = "https://files.pythonhosted.org/packages/71/f2/7bddc7fd612367d1459c5bcf598a9e8f7092d6580d98de0e057eb42697ad/aiohttp-3.13.3-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:34749271508078b261c4abb1767d42b8d0c0cc9449c73a4df494777dc55f0687", size = 1669107 }, + { url = "https://files.pythonhosted.org/packages/00/5a/1aeaecca40e22560f97610a329e0e5efef5e0b5afdf9f857f0d93839ab2e/aiohttp-3.13.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:82611aeec80eb144416956ec85b6ca45a64d76429c1ed46ae1b5f86c6e0c9a26", size = 1760196 }, + { url = "https://files.pythonhosted.org/packages/f8/f8/0ff6992bea7bd560fc510ea1c815f87eedd745fe035589c71ce05612a19a/aiohttp-3.13.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2fff83cfc93f18f215896e3a190e8e5cb413ce01553901aca925176e7568963a", size = 1843591 }, + { url = "https://files.pythonhosted.org/packages/e3/d1/e30e537a15f53485b61f5be525f2157da719819e8377298502aebac45536/aiohttp-3.13.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bbe7d4cecacb439e2e2a8a1a7b935c25b812af7a5fd26503a66dadf428e79ec1", size = 1720277 }, + { url = "https://files.pythonhosted.org/packages/84/45/23f4c451d8192f553d38d838831ebbc156907ea6e05557f39563101b7717/aiohttp-3.13.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b928f30fe49574253644b1ca44b1b8adbd903aa0da4b9054a6c20fc7f4092a25", size = 1548575 }, + { url = "https://files.pythonhosted.org/packages/6a/ed/0a42b127a43712eda7807e7892c083eadfaf8429ca8fb619662a530a3aab/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7b5e8fe4de30df199155baaf64f2fcd604f4c678ed20910db8e2c66dc4b11603", size = 1679455 }, + { url = "https://files.pythonhosted.org/packages/2e/b5/c05f0c2b4b4fe2c9d55e73b6d3ed4fd6c9dc2684b1d81cbdf77e7fad9adb/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:8542f41a62bcc58fc7f11cf7c90e0ec324ce44950003feb70640fc2a9092c32a", size = 1687417 }, + { url = "https://files.pythonhosted.org/packages/c9/6b/915bc5dad66aef602b9e459b5a973529304d4e89ca86999d9d75d80cbd0b/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5e1d8c8b8f1d91cd08d8f4a3c2b067bfca6ec043d3ff36de0f3a715feeedf926", size = 1729968 }, + { url = "https://files.pythonhosted.org/packages/11/3b/e84581290a9520024a08640b63d07673057aec5ca548177a82026187ba73/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:90455115e5da1c3c51ab619ac57f877da8fd6d73c05aacd125c5ae9819582aba", size = 1545690 }, + { url = "https://files.pythonhosted.org/packages/f5/04/0c3655a566c43fd647c81b895dfe361b9f9ad6d58c19309d45cff52d6c3b/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:042e9e0bcb5fba81886c8b4fbb9a09d6b8a00245fd8d88e4d989c1f96c74164c", size = 1746390 }, + { url = "https://files.pythonhosted.org/packages/1f/53/71165b26978f719c3419381514c9690bd5980e764a09440a10bb816ea4ab/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2eb752b102b12a76ca02dff751a801f028b4ffbbc478840b473597fc91a9ed43", size = 1702188 }, + { url = "https://files.pythonhosted.org/packages/29/a7/cbe6c9e8e136314fa1980da388a59d2f35f35395948a08b6747baebb6aa6/aiohttp-3.13.3-cp314-cp314-win32.whl", hash = "sha256:b556c85915d8efaed322bf1bdae9486aa0f3f764195a0fb6ee962e5c71ef5ce1", size = 433126 }, + { url = "https://files.pythonhosted.org/packages/de/56/982704adea7d3b16614fc5936014e9af85c0e34b58f9046655817f04306e/aiohttp-3.13.3-cp314-cp314-win_amd64.whl", hash = "sha256:9bf9f7a65e7aa20dd764151fb3d616c81088f91f8df39c3893a536e279b4b984", size = 459128 }, + { url = "https://files.pythonhosted.org/packages/6c/2a/3c79b638a9c3d4658d345339d22070241ea341ed4e07b5ac60fb0f418003/aiohttp-3.13.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:05861afbbec40650d8a07ea324367cb93e9e8cc7762e04dd4405df99fa65159c", size = 769512 }, + { url = "https://files.pythonhosted.org/packages/29/b9/3e5014d46c0ab0db8707e0ac2711ed28c4da0218c358a4e7c17bae0d8722/aiohttp-3.13.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2fc82186fadc4a8316768d61f3722c230e2c1dcab4200d52d2ebdf2482e47592", size = 506444 }, + { url = "https://files.pythonhosted.org/packages/90/03/c1d4ef9a054e151cd7839cdc497f2638f00b93cbe8043983986630d7a80c/aiohttp-3.13.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0add0900ff220d1d5c5ebbf99ed88b0c1bbf87aa7e4262300ed1376a6b13414f", size = 510798 }, + { url = "https://files.pythonhosted.org/packages/ea/76/8c1e5abbfe8e127c893fe7ead569148a4d5a799f7cf958d8c09f3eedf097/aiohttp-3.13.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:568f416a4072fbfae453dcf9a99194bbb8bdeab718e08ee13dfa2ba0e4bebf29", size = 1868835 }, + { url = "https://files.pythonhosted.org/packages/8e/ac/984c5a6f74c363b01ff97adc96a3976d9c98940b8969a1881575b279ac5d/aiohttp-3.13.3-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:add1da70de90a2569c5e15249ff76a631ccacfe198375eead4aadf3b8dc849dc", size = 1720486 }, + { url = "https://files.pythonhosted.org/packages/b2/9a/b7039c5f099c4eb632138728828b33428585031a1e658d693d41d07d89d1/aiohttp-3.13.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:10b47b7ba335d2e9b1239fa571131a87e2d8ec96b333e68b2a305e7a98b0bae2", size = 1847951 }, + { url = "https://files.pythonhosted.org/packages/3c/02/3bec2b9a1ba3c19ff89a43a19324202b8eb187ca1e928d8bdac9bbdddebd/aiohttp-3.13.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3dd4dce1c718e38081c8f35f323209d4c1df7d4db4bab1b5c88a6b4d12b74587", size = 1941001 }, + { url = "https://files.pythonhosted.org/packages/37/df/d879401cedeef27ac4717f6426c8c36c3091c6e9f08a9178cc87549c537f/aiohttp-3.13.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34bac00a67a812570d4a460447e1e9e06fae622946955f939051e7cc895cfab8", size = 1797246 }, + { url = "https://files.pythonhosted.org/packages/8d/15/be122de1f67e6953add23335c8ece6d314ab67c8bebb3f181063010795a7/aiohttp-3.13.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a19884d2ee70b06d9204b2727a7b9f983d0c684c650254679e716b0b77920632", size = 1627131 }, + { url = "https://files.pythonhosted.org/packages/12/12/70eedcac9134cfa3219ab7af31ea56bc877395b1ac30d65b1bc4b27d0438/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5f8ca7f2bb6ba8348a3614c7918cc4bb73268c5ac2a207576b7afea19d3d9f64", size = 1795196 }, + { url = "https://files.pythonhosted.org/packages/32/11/b30e1b1cd1f3054af86ebe60df96989c6a414dd87e27ad16950eee420bea/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:b0d95340658b9d2f11d9697f59b3814a9d3bb4b7a7c20b131df4bcef464037c0", size = 1782841 }, + { url = "https://files.pythonhosted.org/packages/88/0d/d98a9367b38912384a17e287850f5695c528cff0f14f791ce8ee2e4f7796/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:a1e53262fd202e4b40b70c3aff944a8155059beedc8a89bba9dc1f9ef06a1b56", size = 1795193 }, + { url = "https://files.pythonhosted.org/packages/43/a5/a2dfd1f5ff5581632c7f6a30e1744deda03808974f94f6534241ef60c751/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:d60ac9663f44168038586cab2157e122e46bdef09e9368b37f2d82d354c23f72", size = 1621979 }, + { url = "https://files.pythonhosted.org/packages/fa/f0/12973c382ae7c1cccbc4417e129c5bf54c374dfb85af70893646e1f0e749/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:90751b8eed69435bac9ff4e3d2f6b3af1f57e37ecb0fbeee59c0174c9e2d41df", size = 1822193 }, + { url = "https://files.pythonhosted.org/packages/3c/5f/24155e30ba7f8c96918af1350eb0663e2430aad9e001c0489d89cd708ab1/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fc353029f176fd2b3ec6cfc71be166aba1936fe5d73dd1992ce289ca6647a9aa", size = 1769801 }, + { url = "https://files.pythonhosted.org/packages/eb/f8/7314031ff5c10e6ece114da79b338ec17eeff3a079e53151f7e9f43c4723/aiohttp-3.13.3-cp314-cp314t-win32.whl", hash = "sha256:2e41b18a58da1e474a057b3d35248d8320029f61d70a37629535b16a0c8f3767", size = 466523 }, + { url = "https://files.pythonhosted.org/packages/b4/63/278a98c715ae467624eafe375542d8ba9b4383a016df8fdefe0ae28382a7/aiohttp-3.13.3-cp314-cp314t-win_amd64.whl", hash = "sha256:44531a36aa2264a1860089ffd4dce7baf875ee5a6079d5fb42e261c704ef7344", size = 499694 }, +] + +[[package]] +name = "aiosignal" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "frozenlist" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490 }, +] + +[[package]] +name = "annotated-doc" +version = "0.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303 }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 }, +] + +[[package]] +name = "anyio" +version = "4.12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592 }, +] + +[[package]] +name = "async-timeout" +version = "5.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a5/ae/136395dfbfe00dfc94da3f3e136d0b13f394cba8f4841120e34226265780/async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3", size = 9274 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/ba/e2081de779ca30d473f21f5b30e0e737c438205440784c7dfc81efc2b029/async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c", size = 6233 }, +] + +[[package]] +name = "attrs" +version = "25.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615 }, +] + +[[package]] +name = "certifi" +version = "2026.2.25" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684 }, +] + +[[package]] +name = "cffi" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/d7/516d984057745a6cd96575eea814fe1edd6646ee6efd552fb7b0921dec83/cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44", size = 184283 }, + { url = "https://files.pythonhosted.org/packages/9e/84/ad6a0b408daa859246f57c03efd28e5dd1b33c21737c2db84cae8c237aa5/cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49", size = 180504 }, + { url = "https://files.pythonhosted.org/packages/50/bd/b1a6362b80628111e6653c961f987faa55262b4002fcec42308cad1db680/cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c", size = 208811 }, + { url = "https://files.pythonhosted.org/packages/4f/27/6933a8b2562d7bd1fb595074cf99cc81fc3789f6a6c05cdabb46284a3188/cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb", size = 216402 }, + { url = "https://files.pythonhosted.org/packages/05/eb/b86f2a2645b62adcfff53b0dd97e8dfafb5c8aa864bd0d9a2c2049a0d551/cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0", size = 203217 }, + { url = "https://files.pythonhosted.org/packages/9f/e0/6cbe77a53acf5acc7c08cc186c9928864bd7c005f9efd0d126884858a5fe/cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4", size = 203079 }, + { url = "https://files.pythonhosted.org/packages/98/29/9b366e70e243eb3d14a5cb488dfd3a0b6b2f1fb001a203f653b93ccfac88/cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453", size = 216475 }, + { url = "https://files.pythonhosted.org/packages/21/7a/13b24e70d2f90a322f2900c5d8e1f14fa7e2a6b3332b7309ba7b2ba51a5a/cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495", size = 218829 }, + { url = "https://files.pythonhosted.org/packages/60/99/c9dc110974c59cc981b1f5b66e1d8af8af764e00f0293266824d9c4254bc/cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5", size = 211211 }, + { url = "https://files.pythonhosted.org/packages/49/72/ff2d12dbf21aca1b32a40ed792ee6b40f6dc3a9cf1644bd7ef6e95e0ac5e/cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb", size = 218036 }, + { url = "https://files.pythonhosted.org/packages/e2/cc/027d7fb82e58c48ea717149b03bcadcbdc293553edb283af792bd4bcbb3f/cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a", size = 172184 }, + { url = "https://files.pythonhosted.org/packages/33/fa/072dd15ae27fbb4e06b437eb6e944e75b068deb09e2a2826039e49ee2045/cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739", size = 182790 }, + { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344 }, + { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560 }, + { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613 }, + { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476 }, + { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374 }, + { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597 }, + { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574 }, + { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971 }, + { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972 }, + { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078 }, + { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076 }, + { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820 }, + { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635 }, + { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271 }, + { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048 }, + { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529 }, + { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097 }, + { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983 }, + { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519 }, + { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572 }, + { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963 }, + { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361 }, + { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932 }, + { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557 }, + { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762 }, + { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230 }, + { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043 }, + { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446 }, + { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101 }, + { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948 }, + { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422 }, + { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499 }, + { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928 }, + { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302 }, + { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909 }, + { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402 }, + { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780 }, + { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320 }, + { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487 }, + { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049 }, + { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793 }, + { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300 }, + { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244 }, + { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828 }, + { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926 }, + { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328 }, + { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650 }, + { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687 }, + { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773 }, + { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013 }, + { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593 }, + { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354 }, + { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480 }, + { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584 }, + { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443 }, + { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437 }, + { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487 }, + { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726 }, + { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195 }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/35/02daf95b9cd686320bb622eb148792655c9412dbb9b67abb5694e5910a24/charset_normalizer-3.4.5.tar.gz", hash = "sha256:95adae7b6c42a6c5b5b559b1a99149f090a57128155daeea91732c8d970d8644", size = 134804 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/21/a2b1505639008ba2e6ef03733a81fc6cfd6a07ea6139a2b76421230b8dad/charset_normalizer-3.4.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4167a621a9a1a986c73777dbc15d4b5eac8ac5c10393374109a343d4013ec765", size = 283319 }, + { url = "https://files.pythonhosted.org/packages/70/67/df234c29b68f4e1e095885c9db1cb4b69b8aba49cf94fac041db4aaf1267/charset_normalizer-3.4.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3f64c6bf8f32f9133b668c7f7a7cbdbc453412bc95ecdbd157f3b1e377a92990", size = 189974 }, + { url = "https://files.pythonhosted.org/packages/df/7f/fc66af802961c6be42e2c7b69c58f95cbd1f39b0e81b3365d8efe2a02a04/charset_normalizer-3.4.5-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:568e3c34b58422075a1b49575a6abc616d9751b4d61b23f712e12ebb78fe47b2", size = 207866 }, + { url = "https://files.pythonhosted.org/packages/c9/23/404eb36fac4e95b833c50e305bba9a241086d427bb2167a42eac7c4f7da4/charset_normalizer-3.4.5-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:036c079aa08a6a592b82487f97c60b439428320ed1b2ea0b3912e99d30c77765", size = 203239 }, + { url = "https://files.pythonhosted.org/packages/4b/2f/8a1d989bfadd120c90114ab33e0d2a0cbde05278c1fc15e83e62d570f50a/charset_normalizer-3.4.5-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:340810d34ef83af92148e96e3e44cb2d3f910d2bf95e5618a5c467d9f102231d", size = 196529 }, + { url = "https://files.pythonhosted.org/packages/a5/0c/c75f85ff7ca1f051958bb518cd43922d86f576c03947a050fbedfdfb4f15/charset_normalizer-3.4.5-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:cd2d0f0ec9aa977a27731a3209ebbcacebebaf41f902bd453a928bfd281cf7f8", size = 184152 }, + { url = "https://files.pythonhosted.org/packages/f9/20/4ed37f6199af5dde94d4aeaf577f3813a5ec6635834cda1d957013a09c76/charset_normalizer-3.4.5-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0b362bcd27819f9c07cbf23db4e0e8cd4b44c5ecd900c2ff907b2b92274a7412", size = 195226 }, + { url = "https://files.pythonhosted.org/packages/28/31/7ba1102178cba7c34dcc050f43d427172f389729e356038f0726253dd914/charset_normalizer-3.4.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:77be992288f720306ab4108fe5c74797de327f3248368dfc7e1a916d6ed9e5a2", size = 192933 }, + { url = "https://files.pythonhosted.org/packages/4b/23/f86443ab3921e6a60b33b93f4a1161222231f6c69bc24fb18f3bee7b8518/charset_normalizer-3.4.5-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:8b78d8a609a4b82c273257ee9d631ded7fac0d875bdcdccc109f3ee8328cfcb1", size = 185647 }, + { url = "https://files.pythonhosted.org/packages/82/44/08b8be891760f1f5a6d23ce11d6d50c92981603e6eb740b4f72eea9424e2/charset_normalizer-3.4.5-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:ba20bdf69bd127f66d0174d6f2a93e69045e0b4036dc1ca78e091bcc765830c4", size = 209533 }, + { url = "https://files.pythonhosted.org/packages/3b/5f/df114f23406199f8af711ddccfbf409ffbc5b7cdc18fa19644997ff0c9bb/charset_normalizer-3.4.5-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:76a9d0de4d0eab387822e7b35d8f89367dd237c72e82ab42b9f7bf5e15ada00f", size = 195901 }, + { url = "https://files.pythonhosted.org/packages/07/83/71ef34a76fe8aa05ff8f840244bda2d61e043c2ef6f30d200450b9f6a1be/charset_normalizer-3.4.5-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:8fff79bf5978c693c9b1a4d71e4a94fddfb5fe744eb062a318e15f4a2f63a550", size = 204950 }, + { url = "https://files.pythonhosted.org/packages/58/40/0253be623995365137d7dc68e45245036207ab2227251e69a3d93ce43183/charset_normalizer-3.4.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c7e84e0c0005e3bdc1a9211cd4e62c78ba80bc37b2365ef4410cd2007a9047f2", size = 198546 }, + { url = "https://files.pythonhosted.org/packages/ed/5c/5f3cb5b259a130895ef5ae16b38eaf141430fa3f7af50cd06c5d67e4f7b2/charset_normalizer-3.4.5-cp310-cp310-win32.whl", hash = "sha256:58ad8270cfa5d4bef1bc85bd387217e14ff154d6630e976c6f56f9a040757475", size = 132516 }, + { url = "https://files.pythonhosted.org/packages/a5/c3/84fb174e7770f2df2e1a2115090771bfbc2227fb39a765c6d00568d1aab4/charset_normalizer-3.4.5-cp310-cp310-win_amd64.whl", hash = "sha256:02a9d1b01c1e12c27883b0c9349e0bcd9ae92e727ff1a277207e1a262b1cbf05", size = 142906 }, + { url = "https://files.pythonhosted.org/packages/d7/b2/6f852f8b969f2cbd0d4092d2e60139ab1af95af9bb651337cae89ec0f684/charset_normalizer-3.4.5-cp310-cp310-win_arm64.whl", hash = "sha256:039215608ac7b358c4da0191d10fc76868567fbf276d54c14721bdedeb6de064", size = 133258 }, + { url = "https://files.pythonhosted.org/packages/8f/9e/bcec3b22c64ecec47d39bf5167c2613efd41898c019dccd4183f6aa5d6a7/charset_normalizer-3.4.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:610f72c0ee565dfb8ae1241b666119582fdbfe7c0975c175be719f940e110694", size = 279531 }, + { url = "https://files.pythonhosted.org/packages/58/12/81fd25f7e7078ab5d1eedbb0fac44be4904ae3370a3bf4533c8f2d159acd/charset_normalizer-3.4.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60d68e820af339df4ae8358c7a2e7596badeb61e544438e489035f9fbf3246a5", size = 188006 }, + { url = "https://files.pythonhosted.org/packages/ae/6e/f2d30e8c27c1b0736a6520311982cf5286cfc7f6cac77d7bc1325e3a23f2/charset_normalizer-3.4.5-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:10b473fc8dca1c3ad8559985794815f06ca3fc71942c969129070f2c3cdf7281", size = 205085 }, + { url = "https://files.pythonhosted.org/packages/d0/90/d12cefcb53b5931e2cf792a33718d7126efb116a320eaa0742c7059a95e4/charset_normalizer-3.4.5-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d4eb8ac7469b2a5d64b5b8c04f84d8bf3ad340f4514b98523805cbf46e3b3923", size = 200545 }, + { url = "https://files.pythonhosted.org/packages/03/f4/44d3b830a20e89ff82a3134912d9a1cf6084d64f3b95dcad40f74449a654/charset_normalizer-3.4.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bcb3227c3d9aaf73eaaab1db7ccd80a8995c509ee9941e2aae060ca6e4e5d81", size = 193863 }, + { url = "https://files.pythonhosted.org/packages/25/4b/f212119c18a6320a9d4a730d1b4057875cdeabf21b3614f76549042ef8a8/charset_normalizer-3.4.5-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:75ee9c1cce2911581a70a3c0919d8bccf5b1cbc9b0e5171400ec736b4b569497", size = 181827 }, + { url = "https://files.pythonhosted.org/packages/74/00/b26158e48b425a202a92965f8069e8a63d9af1481dfa206825d7f74d2a3c/charset_normalizer-3.4.5-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1d1401945cb77787dbd3af2446ff2d75912327c4c3a1526ab7955ecf8600687c", size = 191085 }, + { url = "https://files.pythonhosted.org/packages/c4/c2/1c1737bf6fd40335fe53d28fe49afd99ee4143cc57a845e99635ce0b9b6d/charset_normalizer-3.4.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a45e504f5e1be0bd385935a8e1507c442349ca36f511a47057a71c9d1d6ea9e", size = 190688 }, + { url = "https://files.pythonhosted.org/packages/5a/3d/abb5c22dc2ef493cd56522f811246a63c5427c08f3e3e50ab663de27fcf4/charset_normalizer-3.4.5-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:e09f671a54ce70b79a1fc1dc6da3072b7ef7251fadb894ed92d9aa8218465a5f", size = 183077 }, + { url = "https://files.pythonhosted.org/packages/44/33/5298ad4d419a58e25b3508e87f2758d1442ff00c2471f8e0403dab8edad5/charset_normalizer-3.4.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:d01de5e768328646e6a3fa9e562706f8f6641708c115c62588aef2b941a4f88e", size = 206706 }, + { url = "https://files.pythonhosted.org/packages/7b/17/51e7895ac0f87c3b91d276a449ef09f5532a7529818f59646d7a55089432/charset_normalizer-3.4.5-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:131716d6786ad5e3dc542f5cc6f397ba3339dc0fb87f87ac30e550e8987756af", size = 191665 }, + { url = "https://files.pythonhosted.org/packages/90/8f/cce9adf1883e98906dbae380d769b4852bb0fa0004bc7d7a2243418d3ea8/charset_normalizer-3.4.5-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:1a374cc0b88aa710e8865dc1bd6edb3743c59f27830f0293ab101e4cf3ce9f85", size = 201950 }, + { url = "https://files.pythonhosted.org/packages/08/ca/bce99cd5c397a52919e2769d126723f27a4c037130374c051c00470bcd38/charset_normalizer-3.4.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d31f0d1671e1534e395f9eb84a68e0fb670e1edb1fe819a9d7f564ae3bc4e53f", size = 195830 }, + { url = "https://files.pythonhosted.org/packages/87/4f/2e3d023a06911f1281f97b8f036edc9872167036ca6f55cc874a0be6c12c/charset_normalizer-3.4.5-cp311-cp311-win32.whl", hash = "sha256:cace89841c0599d736d3d74a27bc5821288bb47c5441923277afc6059d7fbcb4", size = 132029 }, + { url = "https://files.pythonhosted.org/packages/fe/1f/a853b73d386521fd44b7f67ded6b17b7b2367067d9106a5c4b44f9a34274/charset_normalizer-3.4.5-cp311-cp311-win_amd64.whl", hash = "sha256:f8102ae93c0bc863b1d41ea0f4499c20a83229f52ed870850892df555187154a", size = 142404 }, + { url = "https://files.pythonhosted.org/packages/b4/10/dba36f76b71c38e9d391abe0fd8a5b818790e053c431adecfc98c35cd2a9/charset_normalizer-3.4.5-cp311-cp311-win_arm64.whl", hash = "sha256:ed98364e1c262cf5f9363c3eca8c2df37024f52a8fa1180a3610014f26eac51c", size = 132796 }, + { url = "https://files.pythonhosted.org/packages/9c/b6/9ee9c1a608916ca5feae81a344dffbaa53b26b90be58cc2159e3332d44ec/charset_normalizer-3.4.5-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ed97c282ee4f994ef814042423a529df9497e3c666dca19be1d4cd1129dc7ade", size = 280976 }, + { url = "https://files.pythonhosted.org/packages/f8/d8/a54f7c0b96f1df3563e9190f04daf981e365a9b397eedfdfb5dbef7e5c6c/charset_normalizer-3.4.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0294916d6ccf2d069727d65973c3a1ca477d68708db25fd758dd28b0827cff54", size = 189356 }, + { url = "https://files.pythonhosted.org/packages/42/69/2bf7f76ce1446759a5787cb87d38f6a61eb47dbbdf035cfebf6347292a65/charset_normalizer-3.4.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dc57a0baa3eeedd99fafaef7511b5a6ef4581494e8168ee086031744e2679467", size = 206369 }, + { url = "https://files.pythonhosted.org/packages/10/9c/949d1a46dab56b959d9a87272482195f1840b515a3380e39986989a893ae/charset_normalizer-3.4.5-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ed1a9a204f317ef879b32f9af507d47e49cd5e7f8e8d5d96358c98373314fc60", size = 203285 }, + { url = "https://files.pythonhosted.org/packages/67/5c/ae30362a88b4da237d71ea214a8c7eb915db3eec941adda511729ac25fa2/charset_normalizer-3.4.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7ad83b8f9379176c841f8865884f3514d905bcd2a9a3b210eaa446e7d2223e4d", size = 196274 }, + { url = "https://files.pythonhosted.org/packages/b2/07/c9f2cb0e46cb6d64fdcc4f95953747b843bb2181bda678dc4e699b8f0f9a/charset_normalizer-3.4.5-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:a118e2e0b5ae6b0120d5efa5f866e58f2bb826067a646431da4d6a2bdae7950e", size = 184715 }, + { url = "https://files.pythonhosted.org/packages/36/64/6b0ca95c44fddf692cd06d642b28f63009d0ce325fad6e9b2b4d0ef86a52/charset_normalizer-3.4.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:754f96058e61a5e22e91483f823e07df16416ce76afa4ebf306f8e1d1296d43f", size = 193426 }, + { url = "https://files.pythonhosted.org/packages/50/bc/a730690d726403743795ca3f5bb2baf67838c5fea78236098f324b965e40/charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0c300cefd9b0970381a46394902cd18eaf2aa00163f999590ace991989dcd0fc", size = 191780 }, + { url = "https://files.pythonhosted.org/packages/97/4f/6c0bc9af68222b22951552d73df4532b5be6447cee32d58e7e8c74ecbb7b/charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:c108f8619e504140569ee7de3f97d234f0fbae338a7f9f360455071ef9855a95", size = 185805 }, + { url = "https://files.pythonhosted.org/packages/dd/b9/a523fb9b0ee90814b503452b2600e4cbc118cd68714d57041564886e7325/charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:d1028de43596a315e2720a9849ee79007ab742c06ad8b45a50db8cdb7ed4a82a", size = 208342 }, + { url = "https://files.pythonhosted.org/packages/4d/61/c59e761dee4464050713e50e27b58266cc8e209e518c0b378c1580c959ba/charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:19092dde50335accf365cce21998a1c6dd8eafd42c7b226eb54b2747cdce2fac", size = 193661 }, + { url = "https://files.pythonhosted.org/packages/1c/43/729fa30aad69783f755c5ad8649da17ee095311ca42024742701e202dc59/charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4354e401eb6dab9aed3c7b4030514328a6c748d05e1c3e19175008ca7de84fb1", size = 204819 }, + { url = "https://files.pythonhosted.org/packages/87/33/d9b442ce5a91b96fc0840455a9e49a611bbadae6122778d0a6a79683dd31/charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a68766a3c58fde7f9aaa22b3786276f62ab2f594efb02d0a1421b6282e852e98", size = 198080 }, + { url = "https://files.pythonhosted.org/packages/56/5a/b8b5a23134978ee9885cee2d6995f4c27cc41f9baded0a9685eabc5338f0/charset_normalizer-3.4.5-cp312-cp312-win32.whl", hash = "sha256:1827734a5b308b65ac54e86a618de66f935a4f63a8a462ff1e19a6788d6c2262", size = 132630 }, + { url = "https://files.pythonhosted.org/packages/70/53/e44a4c07e8904500aec95865dc3f6464dc3586a039ef0df606eb3ac38e35/charset_normalizer-3.4.5-cp312-cp312-win_amd64.whl", hash = "sha256:728c6a963dfab66ef865f49286e45239384249672cd598576765acc2a640a636", size = 142856 }, + { url = "https://files.pythonhosted.org/packages/ea/aa/c5628f7cad591b1cf45790b7a61483c3e36cf41349c98af7813c483fd6e8/charset_normalizer-3.4.5-cp312-cp312-win_arm64.whl", hash = "sha256:75dfd1afe0b1647449e852f4fb428195a7ed0588947218f7ba929f6538487f02", size = 132982 }, + { url = "https://files.pythonhosted.org/packages/f5/48/9f34ec4bb24aa3fdba1890c1bddb97c8a4be1bd84ef5c42ac2352563ad05/charset_normalizer-3.4.5-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ac59c15e3f1465f722607800c68713f9fbc2f672b9eb649fe831da4019ae9b23", size = 280788 }, + { url = "https://files.pythonhosted.org/packages/0e/09/6003e7ffeb90cc0560da893e3208396a44c210c5ee42efff539639def59b/charset_normalizer-3.4.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:165c7b21d19365464e8f70e5ce5e12524c58b48c78c1f5a57524603c1ab003f8", size = 188890 }, + { url = "https://files.pythonhosted.org/packages/42/1e/02706edf19e390680daa694d17e2b8eab4b5f7ac285e2a51168b4b22ee6b/charset_normalizer-3.4.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:28269983f25a4da0425743d0d257a2d6921ea7d9b83599d4039486ec5b9f911d", size = 206136 }, + { url = "https://files.pythonhosted.org/packages/c7/87/942c3def1b37baf3cf786bad01249190f3ca3d5e63a84f831e704977de1f/charset_normalizer-3.4.5-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d27ce22ec453564770d29d03a9506d449efbb9fa13c00842262b2f6801c48cce", size = 202551 }, + { url = "https://files.pythonhosted.org/packages/94/0a/af49691938dfe175d71b8a929bd7e4ace2809c0c5134e28bc535660d5262/charset_normalizer-3.4.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0625665e4ebdddb553ab185de5db7054393af8879fb0c87bd5690d14379d6819", size = 195572 }, + { url = "https://files.pythonhosted.org/packages/20/ea/dfb1792a8050a8e694cfbde1570ff97ff74e48afd874152d38163d1df9ae/charset_normalizer-3.4.5-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:c23eb3263356d94858655b3e63f85ac5d50970c6e8febcdde7830209139cc37d", size = 184438 }, + { url = "https://files.pythonhosted.org/packages/72/12/c281e2067466e3ddd0595bfaea58a6946765ace5c72dfa3edc2f5f118026/charset_normalizer-3.4.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e6302ca4ae283deb0af68d2fbf467474b8b6aedcd3dab4db187e07f94c109763", size = 193035 }, + { url = "https://files.pythonhosted.org/packages/ba/4f/3792c056e7708e10464bad0438a44708886fb8f92e3c3d29ec5e2d964d42/charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e51ae7d81c825761d941962450f50d041db028b7278e7b08930b4541b3e45cb9", size = 191340 }, + { url = "https://files.pythonhosted.org/packages/e7/86/80ddba897127b5c7a9bccc481b0cd36c8fefa485d113262f0fe4332f0bf4/charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:597d10dec876923e5c59e48dbd366e852eacb2b806029491d307daea6b917d7c", size = 185464 }, + { url = "https://files.pythonhosted.org/packages/4d/00/b5eff85ba198faacab83e0e4b6f0648155f072278e3b392a82478f8b988b/charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5cffde4032a197bd3b42fd0b9509ec60fb70918d6970e4cc773f20fc9180ca67", size = 208014 }, + { url = "https://files.pythonhosted.org/packages/c8/11/d36f70be01597fd30850dde8a1269ebc8efadd23ba5785808454f2389bde/charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2da4eedcb6338e2321e831a0165759c0c620e37f8cd044a263ff67493be8ffb3", size = 193297 }, + { url = "https://files.pythonhosted.org/packages/1a/1d/259eb0a53d4910536c7c2abb9cb25f4153548efb42800c6a9456764649c0/charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:65a126fb4b070d05340a84fc709dd9e7c75d9b063b610ece8a60197a291d0adf", size = 204321 }, + { url = "https://files.pythonhosted.org/packages/84/31/faa6c5b9d3688715e1ed1bb9d124c384fe2fc1633a409e503ffe1c6398c1/charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c7a80a9242963416bd81f99349d5f3fce1843c303bd404f204918b6d75a75fd6", size = 197509 }, + { url = "https://files.pythonhosted.org/packages/fd/a5/c7d9dd1503ffc08950b3260f5d39ec2366dd08254f0900ecbcf3a6197c7c/charset_normalizer-3.4.5-cp313-cp313-win32.whl", hash = "sha256:f1d725b754e967e648046f00c4facc42d414840f5ccc670c5670f59f83693e4f", size = 132284 }, + { url = "https://files.pythonhosted.org/packages/b9/0f/57072b253af40c8aa6636e6de7d75985624c1eb392815b2f934199340a89/charset_normalizer-3.4.5-cp313-cp313-win_amd64.whl", hash = "sha256:e37bd100d2c5d3ba35db9c7c5ba5a9228cbcffe5c4778dc824b164e5257813d7", size = 142630 }, + { url = "https://files.pythonhosted.org/packages/31/41/1c4b7cc9f13bd9d369ce3bc993e13d374ce25fa38a2663644283ecf422c1/charset_normalizer-3.4.5-cp313-cp313-win_arm64.whl", hash = "sha256:93b3b2cc5cf1b8743660ce77a4f45f3f6d1172068207c1defc779a36eea6bb36", size = 133254 }, + { url = "https://files.pythonhosted.org/packages/43/be/0f0fd9bb4a7fa4fb5067fb7d9ac693d4e928d306f80a0d02bde43a7c4aee/charset_normalizer-3.4.5-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8197abe5ca1ffb7d91e78360f915eef5addff270f8a71c1fc5be24a56f3e4873", size = 280232 }, + { url = "https://files.pythonhosted.org/packages/28/02/983b5445e4bef49cd8c9da73a8e029f0825f39b74a06d201bfaa2e55142a/charset_normalizer-3.4.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2aecdb364b8a1802afdc7f9327d55dad5366bc97d8502d0f5854e50712dbc5f", size = 189688 }, + { url = "https://files.pythonhosted.org/packages/d0/88/152745c5166437687028027dc080e2daed6fe11cfa95a22f4602591c42db/charset_normalizer-3.4.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a66aa5022bf81ab4b1bebfb009db4fd68e0c6d4307a1ce5ef6a26e5878dfc9e4", size = 206833 }, + { url = "https://files.pythonhosted.org/packages/cb/0f/ebc15c8b02af2f19be9678d6eed115feeeccc45ce1f4b098d986c13e8769/charset_normalizer-3.4.5-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d77f97e515688bd615c1d1f795d540f32542d514242067adcb8ef532504cb9ee", size = 202879 }, + { url = "https://files.pythonhosted.org/packages/38/9c/71336bff6934418dc8d1e8a1644176ac9088068bc571da612767619c97b3/charset_normalizer-3.4.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01a1ed54b953303ca7e310fafe0fe347aab348bd81834a0bcd602eb538f89d66", size = 195764 }, + { url = "https://files.pythonhosted.org/packages/b7/95/ce92fde4f98615661871bc282a856cf9b8a15f686ba0af012984660d480b/charset_normalizer-3.4.5-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:b2d37d78297b39a9eb9eb92c0f6df98c706467282055419df141389b23f93362", size = 183728 }, + { url = "https://files.pythonhosted.org/packages/1c/e7/f5b4588d94e747ce45ae680f0f242bc2d98dbd4eccfab73e6160b6893893/charset_normalizer-3.4.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e71bbb595973622b817c042bd943c3f3667e9c9983ce3d205f973f486fec98a7", size = 192937 }, + { url = "https://files.pythonhosted.org/packages/f9/29/9d94ed6b929bf9f48bf6ede6e7474576499f07c4c5e878fb186083622716/charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4cd966c2559f501c6fd69294d082c2934c8dd4719deb32c22961a5ac6db0df1d", size = 192040 }, + { url = "https://files.pythonhosted.org/packages/15/d2/1a093a1cf827957f9445f2fe7298bcc16f8fc5e05c1ed2ad1af0b239035e/charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d5e52d127045d6ae01a1e821acfad2f3a1866c54d0e837828538fabe8d9d1bd6", size = 184107 }, + { url = "https://files.pythonhosted.org/packages/0f/7d/82068ce16bd36135df7b97f6333c5d808b94e01d4599a682e2337ed5fd14/charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:30a2b1a48478c3428d047ed9690d57c23038dac838a87ad624c85c0a78ebeb39", size = 208310 }, + { url = "https://files.pythonhosted.org/packages/84/4e/4dfb52307bb6af4a5c9e73e482d171b81d36f522b21ccd28a49656baa680/charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:d8ed79b8f6372ca4254955005830fd61c1ccdd8c0fac6603e2c145c61dd95db6", size = 192918 }, + { url = "https://files.pythonhosted.org/packages/08/a4/159ff7da662cf7201502ca89980b8f06acf3e887b278956646a8aeb178ab/charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:c5af897b45fa606b12464ccbe0014bbf8c09191e0a66aab6aa9d5cf6e77e0c94", size = 204615 }, + { url = "https://files.pythonhosted.org/packages/d6/62/0dd6172203cb6b429ffffc9935001fde42e5250d57f07b0c28c6046deb6b/charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1088345bcc93c58d8d8f3d783eca4a6e7a7752bbff26c3eee7e73c597c191c2e", size = 197784 }, + { url = "https://files.pythonhosted.org/packages/c7/5e/1aab5cb737039b9c59e63627dc8bbc0d02562a14f831cc450e5f91d84ce1/charset_normalizer-3.4.5-cp314-cp314-win32.whl", hash = "sha256:ee57b926940ba00bca7ba7041e665cc956e55ef482f851b9b65acb20d867e7a2", size = 133009 }, + { url = "https://files.pythonhosted.org/packages/40/65/e7c6c77d7aaa4c0d7974f2e403e17f0ed2cb0fc135f77d686b916bf1eead/charset_normalizer-3.4.5-cp314-cp314-win_amd64.whl", hash = "sha256:4481e6da1830c8a1cc0b746b47f603b653dadb690bcd851d039ffaefe70533aa", size = 143511 }, + { url = "https://files.pythonhosted.org/packages/ba/91/52b0841c71f152f563b8e072896c14e3d83b195c188b338d3cc2e582d1d4/charset_normalizer-3.4.5-cp314-cp314-win_arm64.whl", hash = "sha256:97ab7787092eb9b50fb47fa04f24c75b768a606af1bcba1957f07f128a7219e4", size = 133775 }, + { url = "https://files.pythonhosted.org/packages/c5/60/3a621758945513adfd4db86827a5bafcc615f913dbd0b4c2ed64a65731be/charset_normalizer-3.4.5-py3-none-any.whl", hash = "sha256:9db5e3fcdcee89a78c04dffb3fe33c79f77bd741a624946db2591c81b2fc85b0", size = 55455 }, +] + +[[package]] +name = "click" +version = "8.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274 }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, +] + +[[package]] +name = "curl-cffi" +version = "0.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "cffi" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9b/c9/0067d9a25ed4592b022d4558157fcdb6e123516083700786d38091688767/curl_cffi-0.14.0.tar.gz", hash = "sha256:5ffbc82e59f05008ec08ea432f0e535418823cda44178ee518906a54f27a5f0f", size = 162633 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/aa/f0/0f21e9688eaac85e705537b3a87a5588d0cefb2f09d83e83e0e8be93aa99/curl_cffi-0.14.0-cp39-abi3-macosx_14_0_arm64.whl", hash = "sha256:e35e89c6a69872f9749d6d5fda642ed4fc159619329e99d577d0104c9aad5893", size = 3087277 }, + { url = "https://files.pythonhosted.org/packages/ba/a3/0419bd48fce5b145cb6a2344c6ac17efa588f5b0061f212c88e0723da026/curl_cffi-0.14.0-cp39-abi3-macosx_15_0_x86_64.whl", hash = "sha256:5945478cd28ad7dfb5c54473bcfb6743ee1d66554d57951fdf8fc0e7d8cf4e45", size = 5804650 }, + { url = "https://files.pythonhosted.org/packages/e2/07/a238dd062b7841b8caa2fa8a359eb997147ff3161288f0dd46654d898b4d/curl_cffi-0.14.0-cp39-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c42e8fa3c667db9ccd2e696ee47adcd3cd5b0838d7282f3fc45f6c0ef3cfdfa7", size = 8231918 }, + { url = "https://files.pythonhosted.org/packages/7c/d2/ce907c9b37b5caf76ac08db40cc4ce3d9f94c5500db68a195af3513eacbc/curl_cffi-0.14.0-cp39-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:060fe2c99c41d3cb7f894de318ddf4b0301b08dca70453d769bd4e74b36b8483", size = 8654624 }, + { url = "https://files.pythonhosted.org/packages/f2/ae/6256995b18c75e6ef76b30753a5109e786813aa79088b27c8eabb1ef85c9/curl_cffi-0.14.0-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b158c41a25388690dd0d40b5bc38d1e0f512135f17fdb8029868cbc1993d2e5b", size = 8010654 }, + { url = "https://files.pythonhosted.org/packages/fb/10/ff64249e516b103cb762e0a9dca3ee0f04cf25e2a1d5d9838e0f1273d071/curl_cffi-0.14.0-cp39-abi3-manylinux_2_28_i686.whl", hash = "sha256:1439fbef3500fb723333c826adf0efb0e2e5065a703fb5eccce637a2250db34a", size = 7781969 }, + { url = "https://files.pythonhosted.org/packages/51/76/d6f7bb76c2d12811aa7ff16f5e17b678abdd1b357b9a8ac56310ceccabd5/curl_cffi-0.14.0-cp39-abi3-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e7176f2c2d22b542e3cf261072a81deb018cfa7688930f95dddef215caddb469", size = 7969133 }, + { url = "https://files.pythonhosted.org/packages/23/7c/cca39c0ed4e1772613d3cba13091c0e9d3b89365e84b9bf9838259a3cd8f/curl_cffi-0.14.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:03f21ade2d72978c2bb8670e9b6de5260e2755092b02d94b70b906813662998d", size = 9080167 }, + { url = "https://files.pythonhosted.org/packages/75/03/a942d7119d3e8911094d157598ae0169b1c6ca1bd3f27d7991b279bcc45b/curl_cffi-0.14.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:58ebf02de64ee5c95613209ddacb014c2d2f86298d7080c0a1c12ed876ee0690", size = 9520464 }, + { url = "https://files.pythonhosted.org/packages/a2/77/78900e9b0833066d2274bda75cba426fdb4cef7fbf6a4f6a6ca447607bec/curl_cffi-0.14.0-cp39-abi3-win_amd64.whl", hash = "sha256:6e503f9a103f6ae7acfb3890c843b53ec030785a22ae7682a22cc43afb94123e", size = 1677416 }, + { url = "https://files.pythonhosted.org/packages/5c/7c/d2ba86b0b3e1e2830bd94163d047de122c69a8df03c5c7c36326c456ad82/curl_cffi-0.14.0-cp39-abi3-win_arm64.whl", hash = "sha256:2eed50a969201605c863c4c31269dfc3e0da52916086ac54553cfa353022425c", size = 1425067 }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740 }, +] + +[[package]] +name = "fastapi" +version = "0.135.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "pydantic" }, + { name = "starlette" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e7/7b/f8e0211e9380f7195ba3f3d40c292594fd81ba8ec4629e3854c353aaca45/fastapi-0.135.1.tar.gz", hash = "sha256:d04115b508d936d254cea545b7312ecaa58a7b3a0f84952535b4c9afae7668cd", size = 394962 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/72/42e900510195b23a56bde950d26a51f8b723846bfcaa0286e90287f0422b/fastapi-0.135.1-py3-none-any.whl", hash = "sha256:46e2fc5745924b7c840f71ddd277382af29ce1cdb7d5eab5bf697e3fb9999c9e", size = 116999 }, +] + +[[package]] +name = "frozenlist" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2d/f5/c831fac6cc817d26fd54c7eaccd04ef7e0288806943f7cc5bbf69f3ac1f0/frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad", size = 45875 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/4a/557715d5047da48d54e659203b9335be7bfaafda2c3f627b7c47e0b3aaf3/frozenlist-1.8.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b37f6d31b3dcea7deb5e9696e529a6aa4a898adc33db82da12e4c60a7c4d2011", size = 86230 }, + { url = "https://files.pythonhosted.org/packages/a2/fb/c85f9fed3ea8fe8740e5b46a59cc141c23b842eca617da8876cfce5f760e/frozenlist-1.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ef2b7b394f208233e471abc541cc6991f907ffd47dc72584acee3147899d6565", size = 49621 }, + { url = "https://files.pythonhosted.org/packages/63/70/26ca3f06aace16f2352796b08704338d74b6d1a24ca38f2771afbb7ed915/frozenlist-1.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a88f062f072d1589b7b46e951698950e7da00442fc1cacbe17e19e025dc327ad", size = 49889 }, + { url = "https://files.pythonhosted.org/packages/5d/ed/c7895fd2fde7f3ee70d248175f9b6cdf792fb741ab92dc59cd9ef3bd241b/frozenlist-1.8.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f57fb59d9f385710aa7060e89410aeb5058b99e62f4d16b08b91986b9a2140c2", size = 219464 }, + { url = "https://files.pythonhosted.org/packages/6b/83/4d587dccbfca74cb8b810472392ad62bfa100bf8108c7223eb4c4fa2f7b3/frozenlist-1.8.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:799345ab092bee59f01a915620b5d014698547afd011e691a208637312db9186", size = 221649 }, + { url = "https://files.pythonhosted.org/packages/6a/c6/fd3b9cd046ec5fff9dab66831083bc2077006a874a2d3d9247dea93ddf7e/frozenlist-1.8.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c23c3ff005322a6e16f71bf8692fcf4d5a304aaafe1e262c98c6d4adc7be863e", size = 219188 }, + { url = "https://files.pythonhosted.org/packages/ce/80/6693f55eb2e085fc8afb28cf611448fb5b90e98e068fa1d1b8d8e66e5c7d/frozenlist-1.8.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8a76ea0f0b9dfa06f254ee06053d93a600865b3274358ca48a352ce4f0798450", size = 231748 }, + { url = "https://files.pythonhosted.org/packages/97/d6/e9459f7c5183854abd989ba384fe0cc1a0fb795a83c033f0571ec5933ca4/frozenlist-1.8.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c7366fe1418a6133d5aa824ee53d406550110984de7637d65a178010f759c6ef", size = 236351 }, + { url = "https://files.pythonhosted.org/packages/97/92/24e97474b65c0262e9ecd076e826bfd1d3074adcc165a256e42e7b8a7249/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:13d23a45c4cebade99340c4165bd90eeb4a56c6d8a9d8aa49568cac19a6d0dc4", size = 218767 }, + { url = "https://files.pythonhosted.org/packages/ee/bf/dc394a097508f15abff383c5108cb8ad880d1f64a725ed3b90d5c2fbf0bb/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:e4a3408834f65da56c83528fb52ce7911484f0d1eaf7b761fc66001db1646eff", size = 235887 }, + { url = "https://files.pythonhosted.org/packages/40/90/25b201b9c015dbc999a5baf475a257010471a1fa8c200c843fd4abbee725/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:42145cd2748ca39f32801dad54aeea10039da6f86e303659db90db1c4b614c8c", size = 228785 }, + { url = "https://files.pythonhosted.org/packages/84/f4/b5bc148df03082f05d2dd30c089e269acdbe251ac9a9cf4e727b2dbb8a3d/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:e2de870d16a7a53901e41b64ffdf26f2fbb8917b3e6ebf398098d72c5b20bd7f", size = 230312 }, + { url = "https://files.pythonhosted.org/packages/db/4b/87e95b5d15097c302430e647136b7d7ab2398a702390cf4c8601975709e7/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:20e63c9493d33ee48536600d1a5c95eefc870cd71e7ab037763d1fbb89cc51e7", size = 217650 }, + { url = "https://files.pythonhosted.org/packages/e5/70/78a0315d1fea97120591a83e0acd644da638c872f142fd72a6cebee825f3/frozenlist-1.8.0-cp310-cp310-win32.whl", hash = "sha256:adbeebaebae3526afc3c96fad434367cafbfd1b25d72369a9e5858453b1bb71a", size = 39659 }, + { url = "https://files.pythonhosted.org/packages/66/aa/3f04523fb189a00e147e60c5b2205126118f216b0aa908035c45336e27e4/frozenlist-1.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:667c3777ca571e5dbeb76f331562ff98b957431df140b54c85fd4d52eea8d8f6", size = 43837 }, + { url = "https://files.pythonhosted.org/packages/39/75/1135feecdd7c336938bd55b4dc3b0dfc46d85b9be12ef2628574b28de776/frozenlist-1.8.0-cp310-cp310-win_arm64.whl", hash = "sha256:80f85f0a7cc86e7a54c46d99c9e1318ff01f4687c172ede30fd52d19d1da1c8e", size = 39989 }, + { url = "https://files.pythonhosted.org/packages/bc/03/077f869d540370db12165c0aa51640a873fb661d8b315d1d4d67b284d7ac/frozenlist-1.8.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:09474e9831bc2b2199fad6da3c14c7b0fbdd377cce9d3d77131be28906cb7d84", size = 86912 }, + { url = "https://files.pythonhosted.org/packages/df/b5/7610b6bd13e4ae77b96ba85abea1c8cb249683217ef09ac9e0ae93f25a91/frozenlist-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:17c883ab0ab67200b5f964d2b9ed6b00971917d5d8a92df149dc2c9779208ee9", size = 50046 }, + { url = "https://files.pythonhosted.org/packages/6e/ef/0e8f1fe32f8a53dd26bdd1f9347efe0778b0fddf62789ea683f4cc7d787d/frozenlist-1.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fa47e444b8ba08fffd1c18e8cdb9a75db1b6a27f17507522834ad13ed5922b93", size = 50119 }, + { url = "https://files.pythonhosted.org/packages/11/b1/71a477adc7c36e5fb628245dfbdea2166feae310757dea848d02bd0689fd/frozenlist-1.8.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2552f44204b744fba866e573be4c1f9048d6a324dfe14475103fd51613eb1d1f", size = 231067 }, + { url = "https://files.pythonhosted.org/packages/45/7e/afe40eca3a2dc19b9904c0f5d7edfe82b5304cb831391edec0ac04af94c2/frozenlist-1.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:957e7c38f250991e48a9a73e6423db1bb9dd14e722a10f6b8bb8e16a0f55f695", size = 233160 }, + { url = "https://files.pythonhosted.org/packages/a6/aa/7416eac95603ce428679d273255ffc7c998d4132cfae200103f164b108aa/frozenlist-1.8.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8585e3bb2cdea02fc88ffa245069c36555557ad3609e83be0ec71f54fd4abb52", size = 228544 }, + { url = "https://files.pythonhosted.org/packages/8b/3d/2a2d1f683d55ac7e3875e4263d28410063e738384d3adc294f5ff3d7105e/frozenlist-1.8.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:edee74874ce20a373d62dc28b0b18b93f645633c2943fd90ee9d898550770581", size = 243797 }, + { url = "https://files.pythonhosted.org/packages/78/1e/2d5565b589e580c296d3bb54da08d206e797d941a83a6fdea42af23be79c/frozenlist-1.8.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c9a63152fe95756b85f31186bddf42e4c02c6321207fd6601a1c89ebac4fe567", size = 247923 }, + { url = "https://files.pythonhosted.org/packages/aa/c3/65872fcf1d326a7f101ad4d86285c403c87be7d832b7470b77f6d2ed5ddc/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b6db2185db9be0a04fecf2f241c70b63b1a242e2805be291855078f2b404dd6b", size = 230886 }, + { url = "https://files.pythonhosted.org/packages/a0/76/ac9ced601d62f6956f03cc794f9e04c81719509f85255abf96e2510f4265/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f4be2e3d8bc8aabd566f8d5b8ba7ecc09249d74ba3c9ed52e54dc23a293f0b92", size = 245731 }, + { url = "https://files.pythonhosted.org/packages/b9/49/ecccb5f2598daf0b4a1415497eba4c33c1e8ce07495eb07d2860c731b8d5/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c8d1634419f39ea6f5c427ea2f90ca85126b54b50837f31497f3bf38266e853d", size = 241544 }, + { url = "https://files.pythonhosted.org/packages/53/4b/ddf24113323c0bbcc54cb38c8b8916f1da7165e07b8e24a717b4a12cbf10/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:1a7fa382a4a223773ed64242dbe1c9c326ec09457e6b8428efb4118c685c3dfd", size = 241806 }, + { url = "https://files.pythonhosted.org/packages/a7/fb/9b9a084d73c67175484ba2789a59f8eebebd0827d186a8102005ce41e1ba/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:11847b53d722050808926e785df837353bd4d75f1d494377e59b23594d834967", size = 229382 }, + { url = "https://files.pythonhosted.org/packages/95/a3/c8fb25aac55bf5e12dae5c5aa6a98f85d436c1dc658f21c3ac73f9fa95e5/frozenlist-1.8.0-cp311-cp311-win32.whl", hash = "sha256:27c6e8077956cf73eadd514be8fb04d77fc946a7fe9f7fe167648b0b9085cc25", size = 39647 }, + { url = "https://files.pythonhosted.org/packages/0a/f5/603d0d6a02cfd4c8f2a095a54672b3cf967ad688a60fb9faf04fc4887f65/frozenlist-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:ac913f8403b36a2c8610bbfd25b8013488533e71e62b4b4adce9c86c8cea905b", size = 44064 }, + { url = "https://files.pythonhosted.org/packages/5d/16/c2c9ab44e181f043a86f9a8f84d5124b62dbcb3a02c0977ec72b9ac1d3e0/frozenlist-1.8.0-cp311-cp311-win_arm64.whl", hash = "sha256:d4d3214a0f8394edfa3e303136d0575eece0745ff2b47bd2cb2e66dd92d4351a", size = 39937 }, + { url = "https://files.pythonhosted.org/packages/69/29/948b9aa87e75820a38650af445d2ef2b6b8a6fab1a23b6bb9e4ef0be2d59/frozenlist-1.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1", size = 87782 }, + { url = "https://files.pythonhosted.org/packages/64/80/4f6e318ee2a7c0750ed724fa33a4bdf1eacdc5a39a7a24e818a773cd91af/frozenlist-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b", size = 50594 }, + { url = "https://files.pythonhosted.org/packages/2b/94/5c8a2b50a496b11dd519f4a24cb5496cf125681dd99e94c604ccdea9419a/frozenlist-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4", size = 50448 }, + { url = "https://files.pythonhosted.org/packages/6a/bd/d91c5e39f490a49df14320f4e8c80161cfcce09f1e2cde1edd16a551abb3/frozenlist-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383", size = 242411 }, + { url = "https://files.pythonhosted.org/packages/8f/83/f61505a05109ef3293dfb1ff594d13d64a2324ac3482be2cedc2be818256/frozenlist-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f423a119f4777a4a056b66ce11527366a8bb92f54e541ade21f2374433f6d4", size = 243014 }, + { url = "https://files.pythonhosted.org/packages/d8/cb/cb6c7b0f7d4023ddda30cf56b8b17494eb3a79e3fda666bf735f63118b35/frozenlist-1.8.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3462dd9475af2025c31cc61be6652dfa25cbfb56cbbf52f4ccfe029f38decaf8", size = 234909 }, + { url = "https://files.pythonhosted.org/packages/31/c5/cd7a1f3b8b34af009fb17d4123c5a778b44ae2804e3ad6b86204255f9ec5/frozenlist-1.8.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4c800524c9cd9bac5166cd6f55285957fcfc907db323e193f2afcd4d9abd69b", size = 250049 }, + { url = "https://files.pythonhosted.org/packages/c0/01/2f95d3b416c584a1e7f0e1d6d31998c4a795f7544069ee2e0962a4b60740/frozenlist-1.8.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d6a5df73acd3399d893dafc71663ad22534b5aa4f94e8a2fabfe856c3c1b6a52", size = 256485 }, + { url = "https://files.pythonhosted.org/packages/ce/03/024bf7720b3abaebcff6d0793d73c154237b85bdf67b7ed55e5e9596dc9a/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:405e8fe955c2280ce66428b3ca55e12b3c4e9c336fb2103a4937e891c69a4a29", size = 237619 }, + { url = "https://files.pythonhosted.org/packages/69/fa/f8abdfe7d76b731f5d8bd217827cf6764d4f1d9763407e42717b4bed50a0/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:908bd3f6439f2fef9e85031b59fd4f1297af54415fb60e4254a95f75b3cab3f3", size = 250320 }, + { url = "https://files.pythonhosted.org/packages/f5/3c/b051329f718b463b22613e269ad72138cc256c540f78a6de89452803a47d/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143", size = 246820 }, + { url = "https://files.pythonhosted.org/packages/0f/ae/58282e8f98e444b3f4dd42448ff36fa38bef29e40d40f330b22e7108f565/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608", size = 250518 }, + { url = "https://files.pythonhosted.org/packages/8f/96/007e5944694d66123183845a106547a15944fbbb7154788cbf7272789536/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa", size = 239096 }, + { url = "https://files.pythonhosted.org/packages/66/bb/852b9d6db2fa40be96f29c0d1205c306288f0684df8fd26ca1951d461a56/frozenlist-1.8.0-cp312-cp312-win32.whl", hash = "sha256:433403ae80709741ce34038da08511d4a77062aa924baf411ef73d1146e74faf", size = 39985 }, + { url = "https://files.pythonhosted.org/packages/b8/af/38e51a553dd66eb064cdf193841f16f077585d4d28394c2fa6235cb41765/frozenlist-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746", size = 44591 }, + { url = "https://files.pythonhosted.org/packages/a7/06/1dc65480ab147339fecc70797e9c2f69d9cea9cf38934ce08df070fdb9cb/frozenlist-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:fe3c58d2f5db5fbd18c2987cba06d51b0529f52bc3a6cdc33d3f4eab725104bd", size = 40102 }, + { url = "https://files.pythonhosted.org/packages/2d/40/0832c31a37d60f60ed79e9dfb5a92e1e2af4f40a16a29abcc7992af9edff/frozenlist-1.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a", size = 85717 }, + { url = "https://files.pythonhosted.org/packages/30/ba/b0b3de23f40bc55a7057bd38434e25c34fa48e17f20ee273bbde5e0650f3/frozenlist-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96153e77a591c8adc2ee805756c61f59fef4cf4073a9275ee86fe8cba41241f7", size = 49651 }, + { url = "https://files.pythonhosted.org/packages/0c/ab/6e5080ee374f875296c4243c381bbdef97a9ac39c6e3ce1d5f7d42cb78d6/frozenlist-1.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40", size = 49417 }, + { url = "https://files.pythonhosted.org/packages/d5/4e/e4691508f9477ce67da2015d8c00acd751e6287739123113a9fca6f1604e/frozenlist-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb30f9626572a76dfe4293c7194a09fb1fe93ba94c7d4f720dfae3b646b45027", size = 234391 }, + { url = "https://files.pythonhosted.org/packages/40/76/c202df58e3acdf12969a7895fd6f3bc016c642e6726aa63bd3025e0fc71c/frozenlist-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaa352d7047a31d87dafcacbabe89df0aa506abb5b1b85a2fb91bc3faa02d822", size = 233048 }, + { url = "https://files.pythonhosted.org/packages/f9/c0/8746afb90f17b73ca5979c7a3958116e105ff796e718575175319b5bb4ce/frozenlist-1.8.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:03ae967b4e297f58f8c774c7eabcce57fe3c2434817d4385c50661845a058121", size = 226549 }, + { url = "https://files.pythonhosted.org/packages/7e/eb/4c7eefc718ff72f9b6c4893291abaae5fbc0c82226a32dcd8ef4f7a5dbef/frozenlist-1.8.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6292f1de555ffcc675941d65fffffb0a5bcd992905015f85d0592201793e0e5", size = 239833 }, + { url = "https://files.pythonhosted.org/packages/c2/4e/e5c02187cf704224f8b21bee886f3d713ca379535f16893233b9d672ea71/frozenlist-1.8.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29548f9b5b5e3460ce7378144c3010363d8035cea44bc0bf02d57f5a685e084e", size = 245363 }, + { url = "https://files.pythonhosted.org/packages/1f/96/cb85ec608464472e82ad37a17f844889c36100eed57bea094518bf270692/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ec3cc8c5d4084591b4237c0a272cc4f50a5b03396a47d9caaf76f5d7b38a4f11", size = 229314 }, + { url = "https://files.pythonhosted.org/packages/5d/6f/4ae69c550e4cee66b57887daeebe006fe985917c01d0fff9caab9883f6d0/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:517279f58009d0b1f2e7c1b130b377a349405da3f7621ed6bfae50b10adf20c1", size = 243365 }, + { url = "https://files.pythonhosted.org/packages/7a/58/afd56de246cf11780a40a2c28dc7cbabbf06337cc8ddb1c780a2d97e88d8/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:db1e72ede2d0d7ccb213f218df6a078a9c09a7de257c2fe8fcef16d5925230b1", size = 237763 }, + { url = "https://files.pythonhosted.org/packages/cb/36/cdfaf6ed42e2644740d4a10452d8e97fa1c062e2a8006e4b09f1b5fd7d63/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b4dec9482a65c54a5044486847b8a66bf10c9cb4926d42927ec4e8fd5db7fed8", size = 240110 }, + { url = "https://files.pythonhosted.org/packages/03/a8/9ea226fbefad669f11b52e864c55f0bd57d3c8d7eb07e9f2e9a0b39502e1/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:21900c48ae04d13d416f0e1e0c4d81f7931f73a9dfa0b7a8746fb2fe7dd970ed", size = 233717 }, + { url = "https://files.pythonhosted.org/packages/1e/0b/1b5531611e83ba7d13ccc9988967ea1b51186af64c42b7a7af465dcc9568/frozenlist-1.8.0-cp313-cp313-win32.whl", hash = "sha256:8b7b94a067d1c504ee0b16def57ad5738701e4ba10cec90529f13fa03c833496", size = 39628 }, + { url = "https://files.pythonhosted.org/packages/d8/cf/174c91dbc9cc49bc7b7aab74d8b734e974d1faa8f191c74af9b7e80848e6/frozenlist-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:878be833caa6a3821caf85eb39c5ba92d28e85df26d57afb06b35b2efd937231", size = 43882 }, + { url = "https://files.pythonhosted.org/packages/c1/17/502cd212cbfa96eb1388614fe39a3fc9ab87dbbe042b66f97acb57474834/frozenlist-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:44389d135b3ff43ba8cc89ff7f51f5a0bb6b63d829c8300f79a2fe4fe61bcc62", size = 39676 }, + { url = "https://files.pythonhosted.org/packages/d2/5c/3bbfaa920dfab09e76946a5d2833a7cbdf7b9b4a91c714666ac4855b88b4/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e25ac20a2ef37e91c1b39938b591457666a0fa835c7783c3a8f33ea42870db94", size = 89235 }, + { url = "https://files.pythonhosted.org/packages/d2/d6/f03961ef72166cec1687e84e8925838442b615bd0b8854b54923ce5b7b8a/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07cdca25a91a4386d2e76ad992916a85038a9b97561bf7a3fd12d5d9ce31870c", size = 50742 }, + { url = "https://files.pythonhosted.org/packages/1e/bb/a6d12b7ba4c3337667d0e421f7181c82dda448ce4e7ad7ecd249a16fa806/frozenlist-1.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4e0c11f2cc6717e0a741f84a527c52616140741cd812a50422f83dc31749fb52", size = 51725 }, + { url = "https://files.pythonhosted.org/packages/bc/71/d1fed0ffe2c2ccd70b43714c6cab0f4188f09f8a67a7914a6b46ee30f274/frozenlist-1.8.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b3210649ee28062ea6099cfda39e147fa1bc039583c8ee4481cb7811e2448c51", size = 284533 }, + { url = "https://files.pythonhosted.org/packages/c9/1f/fb1685a7b009d89f9bf78a42d94461bc06581f6e718c39344754a5d9bada/frozenlist-1.8.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581ef5194c48035a7de2aefc72ac6539823bb71508189e5de01d60c9dcd5fa65", size = 292506 }, + { url = "https://files.pythonhosted.org/packages/e6/3b/b991fe1612703f7e0d05c0cf734c1b77aaf7c7d321df4572e8d36e7048c8/frozenlist-1.8.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3ef2d026f16a2b1866e1d86fc4e1291e1ed8a387b2c333809419a2f8b3a77b82", size = 274161 }, + { url = "https://files.pythonhosted.org/packages/ca/ec/c5c618767bcdf66e88945ec0157d7f6c4a1322f1473392319b7a2501ded7/frozenlist-1.8.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5500ef82073f599ac84d888e3a8c1f77ac831183244bfd7f11eaa0289fb30714", size = 294676 }, + { url = "https://files.pythonhosted.org/packages/7c/ce/3934758637d8f8a88d11f0585d6495ef54b2044ed6ec84492a91fa3b27aa/frozenlist-1.8.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50066c3997d0091c411a66e710f4e11752251e6d2d73d70d8d5d4c76442a199d", size = 300638 }, + { url = "https://files.pythonhosted.org/packages/fc/4f/a7e4d0d467298f42de4b41cbc7ddaf19d3cfeabaf9ff97c20c6c7ee409f9/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5c1c8e78426e59b3f8005e9b19f6ff46e5845895adbde20ece9218319eca6506", size = 283067 }, + { url = "https://files.pythonhosted.org/packages/dc/48/c7b163063d55a83772b268e6d1affb960771b0e203b632cfe09522d67ea5/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:eefdba20de0d938cec6a89bd4d70f346a03108a19b9df4248d3cf0d88f1b0f51", size = 292101 }, + { url = "https://files.pythonhosted.org/packages/9f/d0/2366d3c4ecdc2fd391e0afa6e11500bfba0ea772764d631bbf82f0136c9d/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cf253e0e1c3ceb4aaff6df637ce033ff6535fb8c70a764a8f46aafd3d6ab798e", size = 289901 }, + { url = "https://files.pythonhosted.org/packages/b8/94/daff920e82c1b70e3618a2ac39fbc01ae3e2ff6124e80739ce5d71c9b920/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0", size = 289395 }, + { url = "https://files.pythonhosted.org/packages/e3/20/bba307ab4235a09fdcd3cc5508dbabd17c4634a1af4b96e0f69bfe551ebd/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6da155091429aeba16851ecb10a9104a108bcd32f6c1642867eadaee401c1c41", size = 283659 }, + { url = "https://files.pythonhosted.org/packages/fd/00/04ca1c3a7a124b6de4f8a9a17cc2fcad138b4608e7a3fc5877804b8715d7/frozenlist-1.8.0-cp313-cp313t-win32.whl", hash = "sha256:0f96534f8bfebc1a394209427d0f8a63d343c9779cda6fc25e8e121b5fd8555b", size = 43492 }, + { url = "https://files.pythonhosted.org/packages/59/5e/c69f733a86a94ab10f68e496dc6b7e8bc078ebb415281d5698313e3af3a1/frozenlist-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5d63a068f978fc69421fb0e6eb91a9603187527c86b7cd3f534a5b77a592b888", size = 48034 }, + { url = "https://files.pythonhosted.org/packages/16/6c/be9d79775d8abe79b05fa6d23da99ad6e7763a1d080fbae7290b286093fd/frozenlist-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf0a7e10b077bf5fb9380ad3ae8ce20ef919a6ad93b4552896419ac7e1d8e042", size = 41749 }, + { url = "https://files.pythonhosted.org/packages/f1/c8/85da824b7e7b9b6e7f7705b2ecaf9591ba6f79c1177f324c2735e41d36a2/frozenlist-1.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cee686f1f4cadeb2136007ddedd0aaf928ab95216e7691c63e50a8ec066336d0", size = 86127 }, + { url = "https://files.pythonhosted.org/packages/8e/e8/a1185e236ec66c20afd72399522f142c3724c785789255202d27ae992818/frozenlist-1.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:119fb2a1bd47307e899c2fac7f28e85b9a543864df47aa7ec9d3c1b4545f096f", size = 49698 }, + { url = "https://files.pythonhosted.org/packages/a1/93/72b1736d68f03fda5fdf0f2180fb6caaae3894f1b854d006ac61ecc727ee/frozenlist-1.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c", size = 49749 }, + { url = "https://files.pythonhosted.org/packages/a7/b2/fabede9fafd976b991e9f1b9c8c873ed86f202889b864756f240ce6dd855/frozenlist-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:cba69cb73723c3f329622e34bdbf5ce1f80c21c290ff04256cff1cd3c2036ed2", size = 231298 }, + { url = "https://files.pythonhosted.org/packages/3a/3b/d9b1e0b0eed36e70477ffb8360c49c85c8ca8ef9700a4e6711f39a6e8b45/frozenlist-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:778a11b15673f6f1df23d9586f83c4846c471a8af693a22e066508b77d201ec8", size = 232015 }, + { url = "https://files.pythonhosted.org/packages/dc/94/be719d2766c1138148564a3960fc2c06eb688da592bdc25adcf856101be7/frozenlist-1.8.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686", size = 225038 }, + { url = "https://files.pythonhosted.org/packages/e4/09/6712b6c5465f083f52f50cf74167b92d4ea2f50e46a9eea0523d658454ae/frozenlist-1.8.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:97260ff46b207a82a7567b581ab4190bd4dfa09f4db8a8b49d1a958f6aa4940e", size = 240130 }, + { url = "https://files.pythonhosted.org/packages/f8/d4/cd065cdcf21550b54f3ce6a22e143ac9e4836ca42a0de1022da8498eac89/frozenlist-1.8.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54b2077180eb7f83dd52c40b2750d0a9f175e06a42e3213ce047219de902717a", size = 242845 }, + { url = "https://files.pythonhosted.org/packages/62/c3/f57a5c8c70cd1ead3d5d5f776f89d33110b1addae0ab010ad774d9a44fb9/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2f05983daecab868a31e1da44462873306d3cbfd76d1f0b5b69c473d21dbb128", size = 229131 }, + { url = "https://files.pythonhosted.org/packages/6c/52/232476fe9cb64f0742f3fde2b7d26c1dac18b6d62071c74d4ded55e0ef94/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:33f48f51a446114bc5d251fb2954ab0164d5be02ad3382abcbfe07e2531d650f", size = 240542 }, + { url = "https://files.pythonhosted.org/packages/5f/85/07bf3f5d0fb5414aee5f47d33c6f5c77bfe49aac680bfece33d4fdf6a246/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:154e55ec0655291b5dd1b8731c637ecdb50975a2ae70c606d100750a540082f7", size = 237308 }, + { url = "https://files.pythonhosted.org/packages/11/99/ae3a33d5befd41ac0ca2cc7fd3aa707c9c324de2e89db0e0f45db9a64c26/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4314debad13beb564b708b4a496020e5306c7333fa9a3ab90374169a20ffab30", size = 238210 }, + { url = "https://files.pythonhosted.org/packages/b2/60/b1d2da22f4970e7a155f0adde9b1435712ece01b3cd45ba63702aea33938/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:073f8bf8becba60aa931eb3bc420b217bb7d5b8f4750e6f8b3be7f3da85d38b7", size = 231972 }, + { url = "https://files.pythonhosted.org/packages/3f/ab/945b2f32de889993b9c9133216c068b7fcf257d8595a0ac420ac8677cab0/frozenlist-1.8.0-cp314-cp314-win32.whl", hash = "sha256:bac9c42ba2ac65ddc115d930c78d24ab8d4f465fd3fc473cdedfccadb9429806", size = 40536 }, + { url = "https://files.pythonhosted.org/packages/59/ad/9caa9b9c836d9ad6f067157a531ac48b7d36499f5036d4141ce78c230b1b/frozenlist-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0", size = 44330 }, + { url = "https://files.pythonhosted.org/packages/82/13/e6950121764f2676f43534c555249f57030150260aee9dcf7d64efda11dd/frozenlist-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:d1eaff1d00c7751b7c6662e9c5ba6eb2c17a2306ba5e2a37f24ddf3cc953402b", size = 40627 }, + { url = "https://files.pythonhosted.org/packages/c0/c7/43200656ecc4e02d3f8bc248df68256cd9572b3f0017f0a0c4e93440ae23/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d3bb933317c52d7ea5004a1c442eef86f426886fba134ef8cf4226ea6ee1821d", size = 89238 }, + { url = "https://files.pythonhosted.org/packages/d1/29/55c5f0689b9c0fb765055629f472c0de484dcaf0acee2f7707266ae3583c/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8009897cdef112072f93a0efdce29cd819e717fd2f649ee3016efd3cd885a7ed", size = 50738 }, + { url = "https://files.pythonhosted.org/packages/ba/7d/b7282a445956506fa11da8c2db7d276adcbf2b17d8bb8407a47685263f90/frozenlist-1.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2c5dcbbc55383e5883246d11fd179782a9d07a986c40f49abe89ddf865913930", size = 51739 }, + { url = "https://files.pythonhosted.org/packages/62/1c/3d8622e60d0b767a5510d1d3cf21065b9db874696a51ea6d7a43180a259c/frozenlist-1.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:39ecbc32f1390387d2aa4f5a995e465e9e2f79ba3adcac92d68e3e0afae6657c", size = 284186 }, + { url = "https://files.pythonhosted.org/packages/2d/14/aa36d5f85a89679a85a1d44cd7a6657e0b1c75f61e7cad987b203d2daca8/frozenlist-1.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92db2bf818d5cc8d9c1f1fc56b897662e24ea5adb36ad1f1d82875bd64e03c24", size = 292196 }, + { url = "https://files.pythonhosted.org/packages/05/23/6bde59eb55abd407d34f77d39a5126fb7b4f109a3f611d3929f14b700c66/frozenlist-1.8.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dc43a022e555de94c3b68a4ef0b11c4f747d12c024a520c7101709a2144fb37", size = 273830 }, + { url = "https://files.pythonhosted.org/packages/d2/3f/22cff331bfad7a8afa616289000ba793347fcd7bc275f3b28ecea2a27909/frozenlist-1.8.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb89a7f2de3602cfed448095bab3f178399646ab7c61454315089787df07733a", size = 294289 }, + { url = "https://files.pythonhosted.org/packages/a4/89/5b057c799de4838b6c69aa82b79705f2027615e01be996d2486a69ca99c4/frozenlist-1.8.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:33139dc858c580ea50e7e60a1b0ea003efa1fd42e6ec7fdbad78fff65fad2fd2", size = 300318 }, + { url = "https://files.pythonhosted.org/packages/30/de/2c22ab3eb2a8af6d69dc799e48455813bab3690c760de58e1bf43b36da3e/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:168c0969a329b416119507ba30b9ea13688fafffac1b7822802537569a1cb0ef", size = 282814 }, + { url = "https://files.pythonhosted.org/packages/59/f7/970141a6a8dbd7f556d94977858cfb36fa9b66e0892c6dd780d2219d8cd8/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:28bd570e8e189d7f7b001966435f9dac6718324b5be2990ac496cf1ea9ddb7fe", size = 291762 }, + { url = "https://files.pythonhosted.org/packages/c1/15/ca1adae83a719f82df9116d66f5bb28bb95557b3951903d39135620ef157/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b2a095d45c5d46e5e79ba1e5b9cb787f541a8dee0433836cea4b96a2c439dcd8", size = 289470 }, + { url = "https://files.pythonhosted.org/packages/ac/83/dca6dc53bf657d371fbc88ddeb21b79891e747189c5de990b9dfff2ccba1/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:eab8145831a0d56ec9c4139b6c3e594c7a83c2c8be25d5bcf2d86136a532287a", size = 289042 }, + { url = "https://files.pythonhosted.org/packages/96/52/abddd34ca99be142f354398700536c5bd315880ed0a213812bc491cff5e4/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:974b28cf63cc99dfb2188d8d222bc6843656188164848c4f679e63dae4b0708e", size = 283148 }, + { url = "https://files.pythonhosted.org/packages/af/d3/76bd4ed4317e7119c2b7f57c3f6934aba26d277acc6309f873341640e21f/frozenlist-1.8.0-cp314-cp314t-win32.whl", hash = "sha256:342c97bf697ac5480c0a7ec73cd700ecfa5a8a40ac923bd035484616efecc2df", size = 44676 }, + { url = "https://files.pythonhosted.org/packages/89/76/c615883b7b521ead2944bb3480398cbb07e12b7b4e4d073d3752eb721558/frozenlist-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:06be8f67f39c8b1dc671f5d83aaefd3358ae5cdcf8314552c57e7ed3e6475bdd", size = 49451 }, + { url = "https://files.pythonhosted.org/packages/e0/a3/5982da14e113d07b325230f95060e2169f5311b1017ea8af2a29b374c289/frozenlist-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:102e6314ca4da683dca92e3b1355490fed5f313b768500084fbe6371fddfdb79", size = 42507 }, + { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409 }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515 }, +] + +[[package]] +name = "httptools" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/46/120a669232c7bdedb9d52d4aeae7e6c7dfe151e99dc70802e2fc7a5e1993/httptools-0.7.1.tar.gz", hash = "sha256:abd72556974f8e7c74a259655924a717a2365b236c882c3f6f8a45fe94703ac9", size = 258961 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/e5/c07e0bcf4ec8db8164e9f6738c048b2e66aabf30e7506f440c4cc6953f60/httptools-0.7.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:11d01b0ff1fe02c4c32d60af61a4d613b74fad069e47e06e9067758c01e9ac78", size = 204531 }, + { url = "https://files.pythonhosted.org/packages/7e/4f/35e3a63f863a659f92ffd92bef131f3e81cf849af26e6435b49bd9f6f751/httptools-0.7.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:84d86c1e5afdc479a6fdabf570be0d3eb791df0ae727e8dbc0259ed1249998d4", size = 109408 }, + { url = "https://files.pythonhosted.org/packages/f5/71/b0a9193641d9e2471ac541d3b1b869538a5fb6419d52fd2669fa9c79e4b8/httptools-0.7.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c8c751014e13d88d2be5f5f14fc8b89612fcfa92a9cc480f2bc1598357a23a05", size = 440889 }, + { url = "https://files.pythonhosted.org/packages/eb/d9/2e34811397b76718750fea44658cb0205b84566e895192115252e008b152/httptools-0.7.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:654968cb6b6c77e37b832a9be3d3ecabb243bbe7a0b8f65fbc5b6b04c8fcabed", size = 440460 }, + { url = "https://files.pythonhosted.org/packages/01/3f/a04626ebeacc489866bb4d82362c0657b2262bef381d68310134be7f40bb/httptools-0.7.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b580968316348b474b020edf3988eecd5d6eec4634ee6561e72ae3a2a0e00a8a", size = 425267 }, + { url = "https://files.pythonhosted.org/packages/a5/99/adcd4f66614db627b587627c8ad6f4c55f18881549bab10ecf180562e7b9/httptools-0.7.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d496e2f5245319da9d764296e86c5bb6fcf0cf7a8806d3d000717a889c8c0b7b", size = 424429 }, + { url = "https://files.pythonhosted.org/packages/d5/72/ec8fc904a8fd30ba022dfa85f3bbc64c3c7cd75b669e24242c0658e22f3c/httptools-0.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:cbf8317bfccf0fed3b5680c559d3459cccf1abe9039bfa159e62e391c7270568", size = 86173 }, + { url = "https://files.pythonhosted.org/packages/9c/08/17e07e8d89ab8f343c134616d72eebfe03798835058e2ab579dcc8353c06/httptools-0.7.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:474d3b7ab469fefcca3697a10d11a32ee2b9573250206ba1e50d5980910da657", size = 206521 }, + { url = "https://files.pythonhosted.org/packages/aa/06/c9c1b41ff52f16aee526fd10fbda99fa4787938aa776858ddc4a1ea825ec/httptools-0.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3c3b7366bb6c7b96bd72d0dbe7f7d5eead261361f013be5f6d9590465ea1c70", size = 110375 }, + { url = "https://files.pythonhosted.org/packages/cc/cc/10935db22fda0ee34c76f047590ca0a8bd9de531406a3ccb10a90e12ea21/httptools-0.7.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:379b479408b8747f47f3b253326183d7c009a3936518cdb70db58cffd369d9df", size = 456621 }, + { url = "https://files.pythonhosted.org/packages/0e/84/875382b10d271b0c11aa5d414b44f92f8dd53e9b658aec338a79164fa548/httptools-0.7.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cad6b591a682dcc6cf1397c3900527f9affef1e55a06c4547264796bbd17cf5e", size = 454954 }, + { url = "https://files.pythonhosted.org/packages/30/e1/44f89b280f7e46c0b1b2ccee5737d46b3bb13136383958f20b580a821ca0/httptools-0.7.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:eb844698d11433d2139bbeeb56499102143beb582bd6c194e3ba69c22f25c274", size = 440175 }, + { url = "https://files.pythonhosted.org/packages/6f/7e/b9287763159e700e335028bc1824359dc736fa9b829dacedace91a39b37e/httptools-0.7.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f65744d7a8bdb4bda5e1fa23e4ba16832860606fcc09d674d56e425e991539ec", size = 440310 }, + { url = "https://files.pythonhosted.org/packages/b3/07/5b614f592868e07f5c94b1f301b5e14a21df4e8076215a3bccb830a687d8/httptools-0.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:135fbe974b3718eada677229312e97f3b31f8a9c8ffa3ae6f565bf808d5b6bcb", size = 86875 }, + { url = "https://files.pythonhosted.org/packages/53/7f/403e5d787dc4942316e515e949b0c8a013d84078a915910e9f391ba9b3ed/httptools-0.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:38e0c83a2ea9746ebbd643bdfb521b9aa4a91703e2cd705c20443405d2fd16a5", size = 206280 }, + { url = "https://files.pythonhosted.org/packages/2a/0d/7f3fd28e2ce311ccc998c388dd1c53b18120fda3b70ebb022b135dc9839b/httptools-0.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f25bbaf1235e27704f1a7b86cd3304eabc04f569c828101d94a0e605ef7205a5", size = 110004 }, + { url = "https://files.pythonhosted.org/packages/84/a6/b3965e1e146ef5762870bbe76117876ceba51a201e18cc31f5703e454596/httptools-0.7.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c15f37ef679ab9ecc06bfc4e6e8628c32a8e4b305459de7cf6785acd57e4d03", size = 517655 }, + { url = "https://files.pythonhosted.org/packages/11/7d/71fee6f1844e6fa378f2eddde6c3e41ce3a1fb4b2d81118dd544e3441ec0/httptools-0.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7fe6e96090df46b36ccfaf746f03034e5ab723162bc51b0a4cf58305324036f2", size = 511440 }, + { url = "https://files.pythonhosted.org/packages/22/a5/079d216712a4f3ffa24af4a0381b108aa9c45b7a5cc6eb141f81726b1823/httptools-0.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f72fdbae2dbc6e68b8239defb48e6a5937b12218e6ffc2c7846cc37befa84362", size = 495186 }, + { url = "https://files.pythonhosted.org/packages/e9/9e/025ad7b65278745dee3bd0ebf9314934c4592560878308a6121f7f812084/httptools-0.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e99c7b90a29fd82fea9ef57943d501a16f3404d7b9ee81799d41639bdaae412c", size = 499192 }, + { url = "https://files.pythonhosted.org/packages/6d/de/40a8f202b987d43afc4d54689600ff03ce65680ede2f31df348d7f368b8f/httptools-0.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:3e14f530fefa7499334a79b0cf7e7cd2992870eb893526fb097d51b4f2d0f321", size = 86694 }, + { url = "https://files.pythonhosted.org/packages/09/8f/c77b1fcbfd262d422f12da02feb0d218fa228d52485b77b953832105bb90/httptools-0.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6babce6cfa2a99545c60bfef8bee0cc0545413cb0018f617c8059a30ad985de3", size = 202889 }, + { url = "https://files.pythonhosted.org/packages/0a/1a/22887f53602feaa066354867bc49a68fc295c2293433177ee90870a7d517/httptools-0.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:601b7628de7504077dd3dcb3791c6b8694bbd967148a6d1f01806509254fb1ca", size = 108180 }, + { url = "https://files.pythonhosted.org/packages/32/6a/6aaa91937f0010d288d3d124ca2946d48d60c3a5ee7ca62afe870e3ea011/httptools-0.7.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:04c6c0e6c5fb0739c5b8a9eb046d298650a0ff38cf42537fc372b28dc7e4472c", size = 478596 }, + { url = "https://files.pythonhosted.org/packages/6d/70/023d7ce117993107be88d2cbca566a7c1323ccbaf0af7eabf2064fe356f6/httptools-0.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69d4f9705c405ae3ee83d6a12283dc9feba8cc6aaec671b412917e644ab4fa66", size = 473268 }, + { url = "https://files.pythonhosted.org/packages/32/4d/9dd616c38da088e3f436e9a616e1d0cc66544b8cdac405cc4e81c8679fc7/httptools-0.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:44c8f4347d4b31269c8a9205d8a5ee2df5322b09bbbd30f8f862185bb6b05346", size = 455517 }, + { url = "https://files.pythonhosted.org/packages/1d/3a/a6c595c310b7df958e739aae88724e24f9246a514d909547778d776799be/httptools-0.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:465275d76db4d554918aba40bf1cbebe324670f3dfc979eaffaa5d108e2ed650", size = 458337 }, + { url = "https://files.pythonhosted.org/packages/fd/82/88e8d6d2c51edc1cc391b6e044c6c435b6aebe97b1abc33db1b0b24cd582/httptools-0.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:322d00c2068d125bd570f7bf78b2d367dad02b919d8581d7476d8b75b294e3e6", size = 85743 }, + { url = "https://files.pythonhosted.org/packages/34/50/9d095fcbb6de2d523e027a2f304d4551855c2f46e0b82befd718b8b20056/httptools-0.7.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:c08fe65728b8d70b6923ce31e3956f859d5e1e8548e6f22ec520a962c6757270", size = 203619 }, + { url = "https://files.pythonhosted.org/packages/07/f0/89720dc5139ae54b03f861b5e2c55a37dba9a5da7d51e1e824a1f343627f/httptools-0.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7aea2e3c3953521c3c51106ee11487a910d45586e351202474d45472db7d72d3", size = 108714 }, + { url = "https://files.pythonhosted.org/packages/b3/cb/eea88506f191fb552c11787c23f9a405f4c7b0c5799bf73f2249cd4f5228/httptools-0.7.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0e68b8582f4ea9166be62926077a3334064d422cf08ab87d8b74664f8e9058e1", size = 472909 }, + { url = "https://files.pythonhosted.org/packages/e0/4a/a548bdfae6369c0d078bab5769f7b66f17f1bfaa6fa28f81d6be6959066b/httptools-0.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df091cf961a3be783d6aebae963cc9b71e00d57fa6f149025075217bc6a55a7b", size = 470831 }, + { url = "https://files.pythonhosted.org/packages/4d/31/14df99e1c43bd132eec921c2e7e11cda7852f65619bc0fc5bdc2d0cb126c/httptools-0.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f084813239e1eb403ddacd06a30de3d3e09a9b76e7894dcda2b22f8a726e9c60", size = 452631 }, + { url = "https://files.pythonhosted.org/packages/22/d2/b7e131f7be8d854d48cb6d048113c30f9a46dca0c9a8b08fcb3fcd588cdc/httptools-0.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7347714368fb2b335e9063bc2b96f2f87a9ceffcd9758ac295f8bbcd3ffbc0ca", size = 452910 }, + { url = "https://files.pythonhosted.org/packages/53/cf/878f3b91e4e6e011eff6d1fa9ca39f7eb17d19c9d7971b04873734112f30/httptools-0.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:cfabda2a5bb85aa2a904ce06d974a3f30fb36cc63d7feaddec05d2050acede96", size = 88205 }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008 }, +] + +[[package]] +name = "multidict" +version = "6.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1a/c2/c2d94cbe6ac1753f3fc980da97b3d930efe1da3af3c9f5125354436c073d/multidict-6.7.1.tar.gz", hash = "sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d", size = 102010 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/0b/19348d4c98980c4851d2f943f8ebafdece2ae7ef737adcfa5994ce8e5f10/multidict-6.7.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c93c3db7ea657dd4637d57e74ab73de31bccefe144d3d4ce370052035bc85fb5", size = 77176 }, + { url = "https://files.pythonhosted.org/packages/ef/04/9de3f8077852e3d438215c81e9b691244532d2e05b4270e89ce67b7d103c/multidict-6.7.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:974e72a2474600827abaeda71af0c53d9ebbc3c2eb7da37b37d7829ae31232d8", size = 44996 }, + { url = "https://files.pythonhosted.org/packages/31/5c/08c7f7fe311f32e83f7621cd3f99d805f45519cd06fafb247628b861da7d/multidict-6.7.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cdea2e7b2456cfb6694fb113066fd0ec7ea4d67e3a35e1f4cbeea0b448bf5872", size = 44631 }, + { url = "https://files.pythonhosted.org/packages/b7/7f/0e3b1390ae772f27501199996b94b52ceeb64fe6f9120a32c6c3f6b781be/multidict-6.7.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17207077e29342fdc2c9a82e4b306f1127bf1ea91f8b71e02d4798a70bb99991", size = 242561 }, + { url = "https://files.pythonhosted.org/packages/dd/f4/8719f4f167586af317b69dd3e90f913416c91ca610cac79a45c53f590312/multidict-6.7.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4f49cb5661344764e4c7c7973e92a47a59b8fc19b6523649ec9dc4960e58a03", size = 242223 }, + { url = "https://files.pythonhosted.org/packages/47/ab/7c36164cce64a6ad19c6d9a85377b7178ecf3b89f8fd589c73381a5eedfd/multidict-6.7.1-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a9fc4caa29e2e6ae408d1c450ac8bf19892c5fca83ee634ecd88a53332c59981", size = 222322 }, + { url = "https://files.pythonhosted.org/packages/f5/79/a25add6fb38035b5337bc5734f296d9afc99163403bbcf56d4170f97eb62/multidict-6.7.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c5f0c21549ab432b57dcc82130f388d84ad8179824cc3f223d5e7cfbfd4143f6", size = 254005 }, + { url = "https://files.pythonhosted.org/packages/4a/7b/64a87cf98e12f756fc8bd444b001232ffff2be37288f018ad0d3f0aae931/multidict-6.7.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7dfb78d966b2c906ae1d28ccf6e6712a3cd04407ee5088cd276fe8cb42186190", size = 251173 }, + { url = "https://files.pythonhosted.org/packages/4b/ac/b605473de2bb404e742f2cc3583d12aedb2352a70e49ae8fce455b50c5aa/multidict-6.7.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9b0d9b91d1aa44db9c1f1ecd0d9d2ae610b2f4f856448664e01a3b35899f3f92", size = 243273 }, + { url = "https://files.pythonhosted.org/packages/03/65/11492d6a0e259783720f3bc1d9ea55579a76f1407e31ed44045c99542004/multidict-6.7.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dd96c01a9dcd4889dcfcf9eb5544ca0c77603f239e3ffab0524ec17aea9a93ee", size = 238956 }, + { url = "https://files.pythonhosted.org/packages/5f/a7/7ee591302af64e7c196fb63fe856c788993c1372df765102bd0448e7e165/multidict-6.7.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:067343c68cd6612d375710f895337b3a98a033c94f14b9a99eff902f205424e2", size = 233477 }, + { url = "https://files.pythonhosted.org/packages/9c/99/c109962d58756c35fd9992fed7f2355303846ea2ff054bb5f5e9d6b888de/multidict-6.7.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5884a04f4ff56c6120f6ccf703bdeb8b5079d808ba604d4d53aec0d55dc33568", size = 243615 }, + { url = "https://files.pythonhosted.org/packages/d5/5f/1973e7c771c86e93dcfe1c9cc55a5481b610f6614acfc28c0d326fe6bfad/multidict-6.7.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8affcf1c98b82bc901702eb73b6947a1bfa170823c153fe8a47b5f5f02e48e40", size = 249930 }, + { url = "https://files.pythonhosted.org/packages/5d/a5/f170fc2268c3243853580203378cd522446b2df632061e0a5409817854c7/multidict-6.7.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:0d17522c37d03e85c8098ec8431636309b2682cf12e58f4dbc76121fb50e4962", size = 243807 }, + { url = "https://files.pythonhosted.org/packages/de/01/73856fab6d125e5bc652c3986b90e8699a95e84b48d72f39ade6c0e74a8c/multidict-6.7.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:24c0cf81544ca5e17cfcb6e482e7a82cd475925242b308b890c9452a074d4505", size = 239103 }, + { url = "https://files.pythonhosted.org/packages/e7/46/f1220bd9944d8aa40d8ccff100eeeee19b505b857b6f603d6078cb5315b0/multidict-6.7.1-cp310-cp310-win32.whl", hash = "sha256:d82dd730a95e6643802f4454b8fdecdf08667881a9c5670db85bc5a56693f122", size = 41416 }, + { url = "https://files.pythonhosted.org/packages/68/00/9b38e272a770303692fc406c36e1a4c740f401522d5787691eb38a8925a8/multidict-6.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:cf37cbe5ced48d417ba045aca1b21bafca67489452debcde94778a576666a1df", size = 46022 }, + { url = "https://files.pythonhosted.org/packages/64/65/d8d42490c02ee07b6bbe00f7190d70bb4738b3cce7629aaf9f213ef730dd/multidict-6.7.1-cp310-cp310-win_arm64.whl", hash = "sha256:59bc83d3f66b41dac1e7460aac1d196edc70c9ba3094965c467715a70ecb46db", size = 43238 }, + { url = "https://files.pythonhosted.org/packages/ce/f1/a90635c4f88fb913fbf4ce660b83b7445b7a02615bda034b2f8eb38fd597/multidict-6.7.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7ff981b266af91d7b4b3793ca3382e53229088d193a85dfad6f5f4c27fc73e5d", size = 76626 }, + { url = "https://files.pythonhosted.org/packages/a6/9b/267e64eaf6fc637a15b35f5de31a566634a2740f97d8d094a69d34f524a4/multidict-6.7.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:844c5bca0b5444adb44a623fb0a1310c2f4cd41f402126bb269cd44c9b3f3e1e", size = 44706 }, + { url = "https://files.pythonhosted.org/packages/dd/a4/d45caf2b97b035c57267791ecfaafbd59c68212004b3842830954bb4b02e/multidict-6.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f2a0a924d4c2e9afcd7ec64f9de35fcd96915149b2216e1cb2c10a56df483855", size = 44356 }, + { url = "https://files.pythonhosted.org/packages/fd/d2/0a36c8473f0cbaeadd5db6c8b72d15bbceeec275807772bfcd059bef487d/multidict-6.7.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8be1802715a8e892c784c0197c2ace276ea52702a0ede98b6310c8f255a5afb3", size = 244355 }, + { url = "https://files.pythonhosted.org/packages/5d/16/8c65be997fd7dd311b7d39c7b6e71a0cb449bad093761481eccbbe4b42a2/multidict-6.7.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2e2d2ed645ea29f31c4c7ea1552fcfd7cb7ba656e1eafd4134a6620c9f5fdd9e", size = 246433 }, + { url = "https://files.pythonhosted.org/packages/01/fb/4dbd7e848d2799c6a026ec88ad39cf2b8416aa167fcc903baa55ecaa045c/multidict-6.7.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:95922cee9a778659e91db6497596435777bd25ed116701a4c034f8e46544955a", size = 225376 }, + { url = "https://files.pythonhosted.org/packages/b6/8a/4a3a6341eac3830f6053062f8fbc9a9e54407c80755b3f05bc427295c2d0/multidict-6.7.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6b83cabdc375ffaaa15edd97eb7c0c672ad788e2687004990074d7d6c9b140c8", size = 257365 }, + { url = "https://files.pythonhosted.org/packages/f7/a2/dd575a69c1aa206e12d27d0770cdf9b92434b48a9ef0cd0d1afdecaa93c4/multidict-6.7.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:38fb49540705369bab8484db0689d86c0a33a0a9f2c1b197f506b71b4b6c19b0", size = 254747 }, + { url = "https://files.pythonhosted.org/packages/5a/56/21b27c560c13822ed93133f08aa6372c53a8e067f11fbed37b4adcdac922/multidict-6.7.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:439cbebd499f92e9aa6793016a8acaa161dfa749ae86d20960189f5398a19144", size = 246293 }, + { url = "https://files.pythonhosted.org/packages/5a/a4/23466059dc3854763423d0ad6c0f3683a379d97673b1b89ec33826e46728/multidict-6.7.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6d3bc717b6fe763b8be3f2bee2701d3c8eb1b2a8ae9f60910f1b2860c82b6c49", size = 242962 }, + { url = "https://files.pythonhosted.org/packages/1f/67/51dd754a3524d685958001e8fa20a0f5f90a6a856e0a9dcabff69be3dbb7/multidict-6.7.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:619e5a1ac57986dbfec9f0b301d865dddf763696435e2962f6d9cf2fdff2bb71", size = 237360 }, + { url = "https://files.pythonhosted.org/packages/64/3f/036dfc8c174934d4b55d86ff4f978e558b0e585cef70cfc1ad01adc6bf18/multidict-6.7.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0b38ebffd9be37c1170d33bc0f36f4f262e0a09bc1aac1c34c7aa51a7293f0b3", size = 245940 }, + { url = "https://files.pythonhosted.org/packages/3d/20/6214d3c105928ebc353a1c644a6ef1408bc5794fcb4f170bb524a3c16311/multidict-6.7.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:10ae39c9cfe6adedcdb764f5e8411d4a92b055e35573a2eaa88d3323289ef93c", size = 253502 }, + { url = "https://files.pythonhosted.org/packages/b1/e2/c653bc4ae1be70a0f836b82172d643fcf1dade042ba2676ab08ec08bff0f/multidict-6.7.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:25167cc263257660290fba06b9318d2026e3c910be240a146e1f66dd114af2b0", size = 247065 }, + { url = "https://files.pythonhosted.org/packages/c8/11/a854b4154cd3bd8b1fd375e8a8ca9d73be37610c361543d56f764109509b/multidict-6.7.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:128441d052254f42989ef98b7b6a6ecb1e6f708aa962c7984235316db59f50fa", size = 241870 }, + { url = "https://files.pythonhosted.org/packages/13/bf/9676c0392309b5fdae322333d22a829715b570edb9baa8016a517b55b558/multidict-6.7.1-cp311-cp311-win32.whl", hash = "sha256:d62b7f64ffde3b99d06b707a280db04fb3855b55f5a06df387236051d0668f4a", size = 41302 }, + { url = "https://files.pythonhosted.org/packages/c9/68/f16a3a8ba6f7b6dc92a1f19669c0810bd2c43fc5a02da13b1cbf8e253845/multidict-6.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:bdbf9f3b332abd0cdb306e7c2113818ab1e922dc84b8f8fd06ec89ed2a19ab8b", size = 45981 }, + { url = "https://files.pythonhosted.org/packages/ac/ad/9dd5305253fa00cd3c7555dbef69d5bf4133debc53b87ab8d6a44d411665/multidict-6.7.1-cp311-cp311-win_arm64.whl", hash = "sha256:b8c990b037d2fff2f4e33d3f21b9b531c5745b33a49a7d6dbe7a177266af44f6", size = 43159 }, + { url = "https://files.pythonhosted.org/packages/8d/9c/f20e0e2cf80e4b2e4b1c365bf5fe104ee633c751a724246262db8f1a0b13/multidict-6.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a90f75c956e32891a4eda3639ce6dd86e87105271f43d43442a3aedf3cddf172", size = 76893 }, + { url = "https://files.pythonhosted.org/packages/fe/cf/18ef143a81610136d3da8193da9d80bfe1cb548a1e2d1c775f26b23d024a/multidict-6.7.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fccb473e87eaa1382689053e4a4618e7ba7b9b9b8d6adf2027ee474597128cd", size = 45456 }, + { url = "https://files.pythonhosted.org/packages/a9/65/1caac9d4cd32e8433908683446eebc953e82d22b03d10d41a5f0fefe991b/multidict-6.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0fa96985700739c4c7853a43c0b3e169360d6855780021bfc6d0f1ce7c123e7", size = 43872 }, + { url = "https://files.pythonhosted.org/packages/cf/3b/d6bd75dc4f3ff7c73766e04e705b00ed6dbbaccf670d9e05a12b006f5a21/multidict-6.7.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cb2a55f408c3043e42b40cc8eecd575afa27b7e0b956dfb190de0f8499a57a53", size = 251018 }, + { url = "https://files.pythonhosted.org/packages/fd/80/c959c5933adedb9ac15152e4067c702a808ea183a8b64cf8f31af8ad3155/multidict-6.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb0ce7b2a32d09892b3dd6cc44877a0d02a33241fafca5f25c8b6b62374f8b75", size = 258883 }, + { url = "https://files.pythonhosted.org/packages/86/85/7ed40adafea3d4f1c8b916e3b5cc3a8e07dfcdcb9cd72800f4ed3ca1b387/multidict-6.7.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c3a32d23520ee37bf327d1e1a656fec76a2edd5c038bf43eddfa0572ec49c60b", size = 242413 }, + { url = "https://files.pythonhosted.org/packages/d2/57/b8565ff533e48595503c785f8361ff9a4fde4d67de25c207cd0ba3befd03/multidict-6.7.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9c90fed18bffc0189ba814749fdcc102b536e83a9f738a9003e569acd540a733", size = 268404 }, + { url = "https://files.pythonhosted.org/packages/e0/50/9810c5c29350f7258180dfdcb2e52783a0632862eb334c4896ac717cebcb/multidict-6.7.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:da62917e6076f512daccfbbde27f46fed1c98fee202f0559adec8ee0de67f71a", size = 269456 }, + { url = "https://files.pythonhosted.org/packages/f3/8d/5e5be3ced1d12966fefb5c4ea3b2a5b480afcea36406559442c6e31d4a48/multidict-6.7.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfde23ef6ed9db7eaee6c37dcec08524cb43903c60b285b172b6c094711b3961", size = 256322 }, + { url = "https://files.pythonhosted.org/packages/31/6e/d8a26d81ac166a5592782d208dd90dfdc0a7a218adaa52b45a672b46c122/multidict-6.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3758692429e4e32f1ba0df23219cd0b4fc0a52f476726fff9337d1a57676a582", size = 253955 }, + { url = "https://files.pythonhosted.org/packages/59/4c/7c672c8aad41534ba619bcd4ade7a0dc87ed6b8b5c06149b85d3dd03f0cd/multidict-6.7.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:398c1478926eca669f2fd6a5856b6de9c0acf23a2cb59a14c0ba5844fa38077e", size = 251254 }, + { url = "https://files.pythonhosted.org/packages/7b/bd/84c24de512cbafbdbc39439f74e967f19570ce7924e3007174a29c348916/multidict-6.7.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c102791b1c4f3ab36ce4101154549105a53dc828f016356b3e3bcae2e3a039d3", size = 252059 }, + { url = "https://files.pythonhosted.org/packages/fa/ba/f5449385510825b73d01c2d4087bf6d2fccc20a2d42ac34df93191d3dd03/multidict-6.7.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a088b62bd733e2ad12c50dad01b7d0166c30287c166e137433d3b410add807a6", size = 263588 }, + { url = "https://files.pythonhosted.org/packages/d7/11/afc7c677f68f75c84a69fe37184f0f82fce13ce4b92f49f3db280b7e92b3/multidict-6.7.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3d51ff4785d58d3f6c91bdbffcb5e1f7ddfda557727043aa20d20ec4f65e324a", size = 259642 }, + { url = "https://files.pythonhosted.org/packages/2b/17/ebb9644da78c4ab36403739e0e6e0e30ebb135b9caf3440825001a0bddcb/multidict-6.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc5907494fccf3e7d3f94f95c91d6336b092b5fc83811720fae5e2765890dfba", size = 251377 }, + { url = "https://files.pythonhosted.org/packages/ca/a4/840f5b97339e27846c46307f2530a2805d9d537d8b8bd416af031cad7fa0/multidict-6.7.1-cp312-cp312-win32.whl", hash = "sha256:28ca5ce2fd9716631133d0e9a9b9a745ad7f60bac2bccafb56aa380fc0b6c511", size = 41887 }, + { url = "https://files.pythonhosted.org/packages/80/31/0b2517913687895f5904325c2069d6a3b78f66cc641a86a2baf75a05dcbb/multidict-6.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcee94dfbd638784645b066074b338bc9cc155d4b4bffa4adce1615c5a426c19", size = 46053 }, + { url = "https://files.pythonhosted.org/packages/0c/5b/aba28e4ee4006ae4c7df8d327d31025d760ffa992ea23812a601d226e682/multidict-6.7.1-cp312-cp312-win_arm64.whl", hash = "sha256:ba0a9fb644d0c1a2194cf7ffb043bd852cea63a57f66fbd33959f7dae18517bf", size = 43307 }, + { url = "https://files.pythonhosted.org/packages/f2/22/929c141d6c0dba87d3e1d38fbdf1ba8baba86b7776469f2bc2d3227a1e67/multidict-6.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2b41f5fed0ed563624f1c17630cb9941cf2309d4df00e494b551b5f3e3d67a23", size = 76174 }, + { url = "https://files.pythonhosted.org/packages/c7/75/bc704ae15fee974f8fccd871305e254754167dce5f9e42d88a2def741a1d/multidict-6.7.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84e61e3af5463c19b67ced91f6c634effb89ef8bfc5ca0267f954451ed4bb6a2", size = 45116 }, + { url = "https://files.pythonhosted.org/packages/79/76/55cd7186f498ed080a18440c9013011eb548f77ae1b297206d030eb1180a/multidict-6.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:935434b9853c7c112eee7ac891bc4cb86455aa631269ae35442cb316790c1445", size = 43524 }, + { url = "https://files.pythonhosted.org/packages/e9/3c/414842ef8d5a1628d68edee29ba0e5bcf235dbfb3ccd3ea303a7fe8c72ff/multidict-6.7.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:432feb25a1cb67fe82a9680b4d65fb542e4635cb3166cd9c01560651ad60f177", size = 249368 }, + { url = "https://files.pythonhosted.org/packages/f6/32/befed7f74c458b4a525e60519fe8d87eef72bb1e99924fa2b0f9d97a221e/multidict-6.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e82d14e3c948952a1a85503817e038cba5905a3352de76b9a465075d072fba23", size = 256952 }, + { url = "https://files.pythonhosted.org/packages/03/d6/c878a44ba877f366630c860fdf74bfb203c33778f12b6ac274936853c451/multidict-6.7.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4cfb48c6ea66c83bcaaf7e4dfa7ec1b6bbcf751b7db85a328902796dfde4c060", size = 240317 }, + { url = "https://files.pythonhosted.org/packages/68/49/57421b4d7ad2e9e60e25922b08ceb37e077b90444bde6ead629095327a6f/multidict-6.7.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1d540e51b7e8e170174555edecddbd5538105443754539193e3e1061864d444d", size = 267132 }, + { url = "https://files.pythonhosted.org/packages/b7/fe/ec0edd52ddbcea2a2e89e174f0206444a61440b40f39704e64dc807a70bd/multidict-6.7.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:273d23f4b40f3dce4d6c8a821c741a86dec62cded82e1175ba3d99be128147ed", size = 268140 }, + { url = "https://files.pythonhosted.org/packages/b0/73/6e1b01cbeb458807aa0831742232dbdd1fa92bfa33f52a3f176b4ff3dc11/multidict-6.7.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d624335fd4fa1c08a53f8b4be7676ebde19cd092b3895c421045ca87895b429", size = 254277 }, + { url = "https://files.pythonhosted.org/packages/6a/b2/5fb8c124d7561a4974c342bc8c778b471ebbeb3cc17df696f034a7e9afe7/multidict-6.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:12fad252f8b267cc75b66e8fc51b3079604e8d43a75428ffe193cd9e2195dfd6", size = 252291 }, + { url = "https://files.pythonhosted.org/packages/5a/96/51d4e4e06bcce92577fcd488e22600bd38e4fd59c20cb49434d054903bd2/multidict-6.7.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:03ede2a6ffbe8ef936b92cb4529f27f42be7f56afcdab5ab739cd5f27fb1cbf9", size = 250156 }, + { url = "https://files.pythonhosted.org/packages/db/6b/420e173eec5fba721a50e2a9f89eda89d9c98fded1124f8d5c675f7a0c0f/multidict-6.7.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:90efbcf47dbe33dcf643a1e400d67d59abeac5db07dc3f27d6bdeae497a2198c", size = 249742 }, + { url = "https://files.pythonhosted.org/packages/44/a3/ec5b5bd98f306bc2aa297b8c6f11a46714a56b1e6ef5ebda50a4f5d7c5fb/multidict-6.7.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c4b9bfc148f5a91be9244d6264c53035c8a0dcd2f51f1c3c6e30e30ebaa1c84", size = 262221 }, + { url = "https://files.pythonhosted.org/packages/cd/f7/e8c0d0da0cd1e28d10e624604e1a36bcc3353aaebdfdc3a43c72bc683a12/multidict-6.7.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:401c5a650f3add2472d1d288c26deebc540f99e2fb83e9525007a74cd2116f1d", size = 258664 }, + { url = "https://files.pythonhosted.org/packages/52/da/151a44e8016dd33feed44f730bd856a66257c1ee7aed4f44b649fb7edeb3/multidict-6.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:97891f3b1b3ffbded884e2916cacf3c6fc87b66bb0dde46f7357404750559f33", size = 249490 }, + { url = "https://files.pythonhosted.org/packages/87/af/a3b86bf9630b732897f6fc3f4c4714b90aa4361983ccbdcd6c0339b21b0c/multidict-6.7.1-cp313-cp313-win32.whl", hash = "sha256:e1c5988359516095535c4301af38d8a8838534158f649c05dd1050222321bcb3", size = 41695 }, + { url = "https://files.pythonhosted.org/packages/b2/35/e994121b0e90e46134673422dd564623f93304614f5d11886b1b3e06f503/multidict-6.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:960c83bf01a95b12b08fd54324a4eb1d5b52c88932b5cba5d6e712bb3ed12eb5", size = 45884 }, + { url = "https://files.pythonhosted.org/packages/ca/61/42d3e5dbf661242a69c97ea363f2d7b46c567da8eadef8890022be6e2ab0/multidict-6.7.1-cp313-cp313-win_arm64.whl", hash = "sha256:563fe25c678aaba333d5399408f5ec3c383ca5b663e7f774dd179a520b8144df", size = 43122 }, + { url = "https://files.pythonhosted.org/packages/6d/b3/e6b21c6c4f314bb956016b0b3ef2162590a529b84cb831c257519e7fde44/multidict-6.7.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:c76c4bec1538375dad9d452d246ca5368ad6e1c9039dadcf007ae59c70619ea1", size = 83175 }, + { url = "https://files.pythonhosted.org/packages/fb/76/23ecd2abfe0957b234f6c960f4ade497f55f2c16aeb684d4ecdbf1c95791/multidict-6.7.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:57b46b24b5d5ebcc978da4ec23a819a9402b4228b8a90d9c656422b4bdd8a963", size = 48460 }, + { url = "https://files.pythonhosted.org/packages/c4/57/a0ed92b23f3a042c36bc4227b72b97eca803f5f1801c1ab77c8a212d455e/multidict-6.7.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e954b24433c768ce78ab7929e84ccf3422e46deb45a4dc9f93438f8217fa2d34", size = 46930 }, + { url = "https://files.pythonhosted.org/packages/b5/66/02ec7ace29162e447f6382c495dc95826bf931d3818799bbef11e8f7df1a/multidict-6.7.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3bd231490fa7217cc832528e1cd8752a96f0125ddd2b5749390f7c3ec8721b65", size = 242582 }, + { url = "https://files.pythonhosted.org/packages/58/18/64f5a795e7677670e872673aca234162514696274597b3708b2c0d276cce/multidict-6.7.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:253282d70d67885a15c8a7716f3a73edf2d635793ceda8173b9ecc21f2fb8292", size = 250031 }, + { url = "https://files.pythonhosted.org/packages/c8/ed/e192291dbbe51a8290c5686f482084d31bcd9d09af24f63358c3d42fd284/multidict-6.7.1-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b4c48648d7649c9335cf1927a8b87fa692de3dcb15faa676c6a6f1f1aabda43", size = 228596 }, + { url = "https://files.pythonhosted.org/packages/1e/7e/3562a15a60cf747397e7f2180b0a11dc0c38d9175a650e75fa1b4d325e15/multidict-6.7.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:98bc624954ec4d2c7cb074b8eefc2b5d0ce7d482e410df446414355d158fe4ca", size = 257492 }, + { url = "https://files.pythonhosted.org/packages/24/02/7d0f9eae92b5249bb50ac1595b295f10e263dd0078ebb55115c31e0eaccd/multidict-6.7.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1b99af4d9eec0b49927b4402bcbb58dea89d3e0db8806a4086117019939ad3dd", size = 255899 }, + { url = "https://files.pythonhosted.org/packages/00/e3/9b60ed9e23e64c73a5cde95269ef1330678e9c6e34dd4eb6b431b85b5a10/multidict-6.7.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6aac4f16b472d5b7dc6f66a0d49dd57b0e0902090be16594dc9ebfd3d17c47e7", size = 247970 }, + { url = "https://files.pythonhosted.org/packages/3e/06/538e58a63ed5cfb0bd4517e346b91da32fde409d839720f664e9a4ae4f9d/multidict-6.7.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:21f830fe223215dffd51f538e78c172ed7c7f60c9b96a2bf05c4848ad49921c3", size = 245060 }, + { url = "https://files.pythonhosted.org/packages/b2/2f/d743a3045a97c895d401e9bd29aaa09b94f5cbdf1bd561609e5a6c431c70/multidict-6.7.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f5dd81c45b05518b9aa4da4aa74e1c93d715efa234fd3e8a179df611cc85e5f4", size = 235888 }, + { url = "https://files.pythonhosted.org/packages/38/83/5a325cac191ab28b63c52f14f1131f3b0a55ba3b9aa65a6d0bf2a9b921a0/multidict-6.7.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:eb304767bca2bb92fb9c5bd33cedc95baee5bb5f6c88e63706533a1c06ad08c8", size = 243554 }, + { url = "https://files.pythonhosted.org/packages/20/1f/9d2327086bd15da2725ef6aae624208e2ef828ed99892b17f60c344e57ed/multidict-6.7.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c9035dde0f916702850ef66460bc4239d89d08df4d02023a5926e7446724212c", size = 252341 }, + { url = "https://files.pythonhosted.org/packages/e8/2c/2a1aa0280cf579d0f6eed8ee5211c4f1730bd7e06c636ba2ee6aafda302e/multidict-6.7.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:af959b9beeb66c822380f222f0e0a1889331597e81f1ded7f374f3ecb0fd6c52", size = 246391 }, + { url = "https://files.pythonhosted.org/packages/e5/03/7ca022ffc36c5a3f6e03b179a5ceb829be9da5783e6fe395f347c0794680/multidict-6.7.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:41f2952231456154ee479651491e94118229844dd7226541788be783be2b5108", size = 243422 }, + { url = "https://files.pythonhosted.org/packages/dc/1d/b31650eab6c5778aceed46ba735bd97f7c7d2f54b319fa916c0f96e7805b/multidict-6.7.1-cp313-cp313t-win32.whl", hash = "sha256:df9f19c28adcb40b6aae30bbaa1478c389efd50c28d541d76760199fc1037c32", size = 47770 }, + { url = "https://files.pythonhosted.org/packages/ac/5b/2d2d1d522e51285bd61b1e20df8f47ae1a9d80839db0b24ea783b3832832/multidict-6.7.1-cp313-cp313t-win_amd64.whl", hash = "sha256:d54ecf9f301853f2c5e802da559604b3e95bb7a3b01a9c295c6ee591b9882de8", size = 53109 }, + { url = "https://files.pythonhosted.org/packages/3d/a3/cc409ba012c83ca024a308516703cf339bdc4b696195644a7215a5164a24/multidict-6.7.1-cp313-cp313t-win_arm64.whl", hash = "sha256:5a37ca18e360377cfda1d62f5f382ff41f2b8c4ccb329ed974cc2e1643440118", size = 45573 }, + { url = "https://files.pythonhosted.org/packages/91/cc/db74228a8be41884a567e88a62fd589a913708fcf180d029898c17a9a371/multidict-6.7.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8f333ec9c5eb1b7105e3b84b53141e66ca05a19a605368c55450b6ba208cb9ee", size = 75190 }, + { url = "https://files.pythonhosted.org/packages/d5/22/492f2246bb5b534abd44804292e81eeaf835388901f0c574bac4eeec73c5/multidict-6.7.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a407f13c188f804c759fc6a9f88286a565c242a76b27626594c133b82883b5c2", size = 44486 }, + { url = "https://files.pythonhosted.org/packages/f1/4f/733c48f270565d78b4544f2baddc2fb2a245e5a8640254b12c36ac7ac68e/multidict-6.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0e161ddf326db5577c3a4cc2d8648f81456e8a20d40415541587a71620d7a7d1", size = 43219 }, + { url = "https://files.pythonhosted.org/packages/24/bb/2c0c2287963f4259c85e8bcbba9182ced8d7fca65c780c38e99e61629d11/multidict-6.7.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1e3a8bb24342a8201d178c3b4984c26ba81a577c80d4d525727427460a50c22d", size = 245132 }, + { url = "https://files.pythonhosted.org/packages/a7/f9/44d4b3064c65079d2467888794dea218d1601898ac50222ab8a9a8094460/multidict-6.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97231140a50f5d447d3164f994b86a0bed7cd016e2682f8650d6a9158e14fd31", size = 252420 }, + { url = "https://files.pythonhosted.org/packages/8b/13/78f7275e73fa17b24c9a51b0bd9d73ba64bb32d0ed51b02a746eb876abe7/multidict-6.7.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b10359683bd8806a200fd2909e7c8ca3a7b24ec1d8132e483d58e791d881048", size = 233510 }, + { url = "https://files.pythonhosted.org/packages/4b/25/8167187f62ae3cbd52da7893f58cb036b47ea3fb67138787c76800158982/multidict-6.7.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:283ddac99f7ac25a4acadbf004cb5ae34480bbeb063520f70ce397b281859362", size = 264094 }, + { url = "https://files.pythonhosted.org/packages/a1/e7/69a3a83b7b030cf283fb06ce074a05a02322359783424d7edf0f15fe5022/multidict-6.7.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:538cec1e18c067d0e6103aa9a74f9e832904c957adc260e61cd9d8cf0c3b3d37", size = 260786 }, + { url = "https://files.pythonhosted.org/packages/fe/3b/8ec5074bcfc450fe84273713b4b0a0dd47c0249358f5d82eb8104ffe2520/multidict-6.7.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7eee46ccb30ff48a1e35bb818cc90846c6be2b68240e42a78599166722cea709", size = 248483 }, + { url = "https://files.pythonhosted.org/packages/48/5a/d5a99e3acbca0e29c5d9cba8f92ceb15dce78bab963b308ae692981e3a5d/multidict-6.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa263a02f4f2dd2d11a7b1bb4362aa7cb1049f84a9235d31adf63f30143469a0", size = 248403 }, + { url = "https://files.pythonhosted.org/packages/35/48/e58cd31f6c7d5102f2a4bf89f96b9cf7e00b6c6f3d04ecc44417c00a5a3c/multidict-6.7.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:2e1425e2f99ec5bd36c15a01b690a1a2456209c5deed58f95469ffb46039ccbb", size = 240315 }, + { url = "https://files.pythonhosted.org/packages/94/33/1cd210229559cb90b6786c30676bb0c58249ff42f942765f88793b41fdce/multidict-6.7.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:497394b3239fc6f0e13a78a3e1b61296e72bf1c5f94b4c4eb80b265c37a131cd", size = 245528 }, + { url = "https://files.pythonhosted.org/packages/64/f2/6e1107d226278c876c783056b7db43d800bb64c6131cec9c8dfb6903698e/multidict-6.7.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:233b398c29d3f1b9676b4b6f75c518a06fcb2ea0b925119fb2c1bc35c05e1601", size = 258784 }, + { url = "https://files.pythonhosted.org/packages/4d/c1/11f664f14d525e4a1b5327a82d4de61a1db604ab34c6603bb3c2cc63ad34/multidict-6.7.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:93b1818e4a6e0930454f0f2af7dfce69307ca03cdcfb3739bf4d91241967b6c1", size = 251980 }, + { url = "https://files.pythonhosted.org/packages/e1/9f/75a9ac888121d0c5bbd4ecf4eead45668b1766f6baabfb3b7f66a410e231/multidict-6.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f33dc2a3abe9249ea5d8360f969ec7f4142e7ac45ee7014d8f8d5acddf178b7b", size = 243602 }, + { url = "https://files.pythonhosted.org/packages/9a/e7/50bf7b004cc8525d80dbbbedfdc7aed3e4c323810890be4413e589074032/multidict-6.7.1-cp314-cp314-win32.whl", hash = "sha256:3ab8b9d8b75aef9df299595d5388b14530839f6422333357af1339443cff777d", size = 40930 }, + { url = "https://files.pythonhosted.org/packages/e0/bf/52f25716bbe93745595800f36fb17b73711f14da59ed0bb2eba141bc9f0f/multidict-6.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:5e01429a929600e7dab7b166062d9bb54a5eed752384c7384c968c2afab8f50f", size = 45074 }, + { url = "https://files.pythonhosted.org/packages/97/ab/22803b03285fa3a525f48217963da3a65ae40f6a1b6f6cf2768879e208f9/multidict-6.7.1-cp314-cp314-win_arm64.whl", hash = "sha256:4885cb0e817aef5d00a2e8451d4665c1808378dc27c2705f1bf4ef8505c0d2e5", size = 42471 }, + { url = "https://files.pythonhosted.org/packages/e0/6d/f9293baa6146ba9507e360ea0292b6422b016907c393e2f63fc40ab7b7b5/multidict-6.7.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0458c978acd8e6ea53c81eefaddbbee9c6c5e591f41b3f5e8e194780fe026581", size = 82401 }, + { url = "https://files.pythonhosted.org/packages/7a/68/53b5494738d83558d87c3c71a486504d8373421c3e0dbb6d0db48ad42ee0/multidict-6.7.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c0abd12629b0af3cf590982c0b413b1e7395cd4ec026f30986818ab95bfaa94a", size = 48143 }, + { url = "https://files.pythonhosted.org/packages/37/e8/5284c53310dcdc99ce5d66563f6e5773531a9b9fe9ec7a615e9bc306b05f/multidict-6.7.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:14525a5f61d7d0c94b368a42cff4c9a4e7ba2d52e2672a7b23d84dc86fb02b0c", size = 46507 }, + { url = "https://files.pythonhosted.org/packages/e4/fc/6800d0e5b3875568b4083ecf5f310dcf91d86d52573160834fb4bfcf5e4f/multidict-6.7.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17307b22c217b4cf05033dabefe68255a534d637c6c9b0cc8382718f87be4262", size = 239358 }, + { url = "https://files.pythonhosted.org/packages/41/75/4ad0973179361cdf3a113905e6e088173198349131be2b390f9fa4da5fc6/multidict-6.7.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a7e590ff876a3eaf1c02a4dfe0724b6e69a9e9de6d8f556816f29c496046e59", size = 246884 }, + { url = "https://files.pythonhosted.org/packages/c3/9c/095bb28b5da139bd41fb9a5d5caff412584f377914bd8787c2aa98717130/multidict-6.7.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5fa6a95dfee63893d80a34758cd0e0c118a30b8dcb46372bf75106c591b77889", size = 225878 }, + { url = "https://files.pythonhosted.org/packages/07/d0/c0a72000243756e8f5a277b6b514fa005f2c73d481b7d9e47cd4568aa2e4/multidict-6.7.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a0543217a6a017692aa6ae5cc39adb75e587af0f3a82288b1492eb73dd6cc2a4", size = 253542 }, + { url = "https://files.pythonhosted.org/packages/c0/6b/f69da15289e384ecf2a68837ec8b5ad8c33e973aa18b266f50fe55f24b8c/multidict-6.7.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f99fe611c312b3c1c0ace793f92464d8cd263cc3b26b5721950d977b006b6c4d", size = 252403 }, + { url = "https://files.pythonhosted.org/packages/a2/76/b9669547afa5a1a25cd93eaca91c0da1c095b06b6d2d8ec25b713588d3a1/multidict-6.7.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9004d8386d133b7e6135679424c91b0b854d2d164af6ea3f289f8f2761064609", size = 244889 }, + { url = "https://files.pythonhosted.org/packages/7e/a9/a50d2669e506dad33cfc45b5d574a205587b7b8a5f426f2fbb2e90882588/multidict-6.7.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e628ef0e6859ffd8273c69412a2465c4be4a9517d07261b33334b5ec6f3c7489", size = 241982 }, + { url = "https://files.pythonhosted.org/packages/c5/bb/1609558ad8b456b4827d3c5a5b775c93b87878fd3117ed3db3423dfbce1b/multidict-6.7.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:841189848ba629c3552035a6a7f5bf3b02eb304e9fea7492ca220a8eda6b0e5c", size = 232415 }, + { url = "https://files.pythonhosted.org/packages/d8/59/6f61039d2aa9261871e03ab9dc058a550d240f25859b05b67fd70f80d4b3/multidict-6.7.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ce1bbd7d780bb5a0da032e095c951f7014d6b0a205f8318308140f1a6aba159e", size = 240337 }, + { url = "https://files.pythonhosted.org/packages/a1/29/fdc6a43c203890dc2ae9249971ecd0c41deaedfe00d25cb6564b2edd99eb/multidict-6.7.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b26684587228afed0d50cf804cc71062cc9c1cdf55051c4c6345d372947b268c", size = 248788 }, + { url = "https://files.pythonhosted.org/packages/a9/14/a153a06101323e4cf086ecee3faadba52ff71633d471f9685c42e3736163/multidict-6.7.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9f9af11306994335398293f9958071019e3ab95e9a707dc1383a35613f6abcb9", size = 242842 }, + { url = "https://files.pythonhosted.org/packages/41/5f/604ae839e64a4a6efc80db94465348d3b328ee955e37acb24badbcd24d83/multidict-6.7.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b4938326284c4f1224178a560987b6cf8b4d38458b113d9b8c1db1a836e640a2", size = 240237 }, + { url = "https://files.pythonhosted.org/packages/5f/60/c3a5187bf66f6fb546ff4ab8fb5a077cbdd832d7b1908d4365c7f74a1917/multidict-6.7.1-cp314-cp314t-win32.whl", hash = "sha256:98655c737850c064a65e006a3df7c997cd3b220be4ec8fe26215760b9697d4d7", size = 48008 }, + { url = "https://files.pythonhosted.org/packages/0c/f7/addf1087b860ac60e6f382240f64fb99f8bfb532bb06f7c542b83c29ca61/multidict-6.7.1-cp314-cp314t-win_amd64.whl", hash = "sha256:497bde6223c212ba11d462853cfa4f0ae6ef97465033e7dc9940cdb3ab5b48e5", size = 53542 }, + { url = "https://files.pythonhosted.org/packages/4c/81/4629d0aa32302ef7b2ec65c75a728cc5ff4fa410c50096174c1632e70b3e/multidict-6.7.1-cp314-cp314t-win_arm64.whl", hash = "sha256:2bbd113e0d4af5db41d5ebfe9ccaff89de2120578164f86a5d17d5a576d1e5b2", size = 44719 }, + { url = "https://files.pythonhosted.org/packages/81/08/7036c080d7117f28a4af526d794aab6a84463126db031b007717c1a6676e/multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", size = 12319 }, +] + +[[package]] +name = "openai-pool-orchestrator" +version = "2.0.0" +source = { editable = "." } +dependencies = [ + { name = "aiohttp" }, + { name = "curl-cffi" }, + { name = "fastapi" }, + { name = "requests" }, + { name = "uvicorn", extra = ["standard"] }, +] + +[package.metadata] +requires-dist = [ + { name = "aiohttp", specifier = ">=3.9" }, + { name = "curl-cffi", specifier = ">=0.6" }, + { name = "fastapi", specifier = ">=0.110" }, + { name = "requests", specifier = ">=2.31" }, + { name = "uvicorn", extras = ["standard"], specifier = ">=0.27" }, +] + +[[package]] +name = "propcache" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9e/da/e9fc233cf63743258bff22b3dfa7ea5baef7b5bc324af47a0ad89b8ffc6f/propcache-0.4.1.tar.gz", hash = "sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d", size = 46442 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/0e/934b541323035566a9af292dba85a195f7b78179114f2c6ebb24551118a9/propcache-0.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c2d1fa3201efaf55d730400d945b5b3ab6e672e100ba0f9a409d950ab25d7db", size = 79534 }, + { url = "https://files.pythonhosted.org/packages/a1/6b/db0d03d96726d995dc7171286c6ba9d8d14251f37433890f88368951a44e/propcache-0.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1eb2994229cc8ce7fe9b3db88f5465f5fd8651672840b2e426b88cdb1a30aac8", size = 45526 }, + { url = "https://files.pythonhosted.org/packages/e4/c3/82728404aea669e1600f304f2609cde9e665c18df5a11cdd57ed73c1dceb/propcache-0.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:66c1f011f45a3b33d7bcb22daed4b29c0c9e2224758b6be00686731e1b46f925", size = 47263 }, + { url = "https://files.pythonhosted.org/packages/df/1b/39313ddad2bf9187a1432654c38249bab4562ef535ef07f5eb6eb04d0b1b/propcache-0.4.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9a52009f2adffe195d0b605c25ec929d26b36ef986ba85244891dee3b294df21", size = 201012 }, + { url = "https://files.pythonhosted.org/packages/5b/01/f1d0b57d136f294a142acf97f4ed58c8e5b974c21e543000968357115011/propcache-0.4.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5d4e2366a9c7b837555cf02fb9be2e3167d333aff716332ef1b7c3a142ec40c5", size = 209491 }, + { url = "https://files.pythonhosted.org/packages/a1/c8/038d909c61c5bb039070b3fb02ad5cccdb1dde0d714792e251cdb17c9c05/propcache-0.4.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9d2b6caef873b4f09e26ea7e33d65f42b944837563a47a94719cc3544319a0db", size = 215319 }, + { url = "https://files.pythonhosted.org/packages/08/57/8c87e93142b2c1fa2408e45695205a7ba05fb5db458c0bf5c06ba0e09ea6/propcache-0.4.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2b16ec437a8c8a965ecf95739448dd938b5c7f56e67ea009f4300d8df05f32b7", size = 196856 }, + { url = "https://files.pythonhosted.org/packages/42/df/5615fec76aa561987a534759b3686008a288e73107faa49a8ae5795a9f7a/propcache-0.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:296f4c8ed03ca7476813fe666c9ea97869a8d7aec972618671b33a38a5182ef4", size = 193241 }, + { url = "https://files.pythonhosted.org/packages/d5/21/62949eb3a7a54afe8327011c90aca7e03547787a88fb8bd9726806482fea/propcache-0.4.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:1f0978529a418ebd1f49dad413a2b68af33f85d5c5ca5c6ca2a3bed375a7ac60", size = 190552 }, + { url = "https://files.pythonhosted.org/packages/30/ee/ab4d727dd70806e5b4de96a798ae7ac6e4d42516f030ee60522474b6b332/propcache-0.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fd138803047fb4c062b1c1dd95462f5209456bfab55c734458f15d11da288f8f", size = 200113 }, + { url = "https://files.pythonhosted.org/packages/8a/0b/38b46208e6711b016aa8966a3ac793eee0d05c7159d8342aa27fc0bc365e/propcache-0.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:8c9b3cbe4584636d72ff556d9036e0c9317fa27b3ac1f0f558e7e84d1c9c5900", size = 200778 }, + { url = "https://files.pythonhosted.org/packages/cf/81/5abec54355ed344476bee711e9f04815d4b00a311ab0535599204eecc257/propcache-0.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f93243fdc5657247533273ac4f86ae106cc6445a0efacb9a1bfe982fcfefd90c", size = 193047 }, + { url = "https://files.pythonhosted.org/packages/ec/b6/1f237c04e32063cb034acd5f6ef34ef3a394f75502e72703545631ab1ef6/propcache-0.4.1-cp310-cp310-win32.whl", hash = "sha256:a0ee98db9c5f80785b266eb805016e36058ac72c51a064040f2bc43b61101cdb", size = 38093 }, + { url = "https://files.pythonhosted.org/packages/a6/67/354aac4e0603a15f76439caf0427781bcd6797f370377f75a642133bc954/propcache-0.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:1cdb7988c4e5ac7f6d175a28a9aa0c94cb6f2ebe52756a3c0cda98d2809a9e37", size = 41638 }, + { url = "https://files.pythonhosted.org/packages/e0/e1/74e55b9fd1a4c209ff1a9a824bf6c8b3d1fc5a1ac3eabe23462637466785/propcache-0.4.1-cp310-cp310-win_arm64.whl", hash = "sha256:d82ad62b19645419fe79dd63b3f9253e15b30e955c0170e5cebc350c1844e581", size = 38229 }, + { url = "https://files.pythonhosted.org/packages/8c/d4/4e2c9aaf7ac2242b9358f98dccd8f90f2605402f5afeff6c578682c2c491/propcache-0.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:60a8fda9644b7dfd5dece8c61d8a85e271cb958075bfc4e01083c148b61a7caf", size = 80208 }, + { url = "https://files.pythonhosted.org/packages/c2/21/d7b68e911f9c8e18e4ae43bdbc1e1e9bbd971f8866eb81608947b6f585ff/propcache-0.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c30b53e7e6bda1d547cabb47c825f3843a0a1a42b0496087bb58d8fedf9f41b5", size = 45777 }, + { url = "https://files.pythonhosted.org/packages/d3/1d/11605e99ac8ea9435651ee71ab4cb4bf03f0949586246476a25aadfec54a/propcache-0.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6918ecbd897443087a3b7cd978d56546a812517dcaaca51b49526720571fa93e", size = 47647 }, + { url = "https://files.pythonhosted.org/packages/58/1a/3c62c127a8466c9c843bccb503d40a273e5cc69838805f322e2826509e0d/propcache-0.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3d902a36df4e5989763425a8ab9e98cd8ad5c52c823b34ee7ef307fd50582566", size = 214929 }, + { url = "https://files.pythonhosted.org/packages/56/b9/8fa98f850960b367c4b8fe0592e7fc341daa7a9462e925228f10a60cf74f/propcache-0.4.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a9695397f85973bb40427dedddf70d8dc4a44b22f1650dd4af9eedf443d45165", size = 221778 }, + { url = "https://files.pythonhosted.org/packages/46/a6/0ab4f660eb59649d14b3d3d65c439421cf2f87fe5dd68591cbe3c1e78a89/propcache-0.4.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2bb07ffd7eaad486576430c89f9b215f9e4be68c4866a96e97db9e97fead85dc", size = 228144 }, + { url = "https://files.pythonhosted.org/packages/52/6a/57f43e054fb3d3a56ac9fc532bc684fc6169a26c75c353e65425b3e56eef/propcache-0.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd6f30fdcf9ae2a70abd34da54f18da086160e4d7d9251f81f3da0ff84fc5a48", size = 210030 }, + { url = "https://files.pythonhosted.org/packages/40/e2/27e6feebb5f6b8408fa29f5efbb765cd54c153ac77314d27e457a3e993b7/propcache-0.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fc38cba02d1acba4e2869eef1a57a43dfbd3d49a59bf90dda7444ec2be6a5570", size = 208252 }, + { url = "https://files.pythonhosted.org/packages/9e/f8/91c27b22ccda1dbc7967f921c42825564fa5336a01ecd72eb78a9f4f53c2/propcache-0.4.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:67fad6162281e80e882fb3ec355398cf72864a54069d060321f6cd0ade95fe85", size = 202064 }, + { url = "https://files.pythonhosted.org/packages/f2/26/7f00bd6bd1adba5aafe5f4a66390f243acab58eab24ff1a08bebb2ef9d40/propcache-0.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f10207adf04d08bec185bae14d9606a1444715bc99180f9331c9c02093e1959e", size = 212429 }, + { url = "https://files.pythonhosted.org/packages/84/89/fd108ba7815c1117ddca79c228f3f8a15fc82a73bca8b142eb5de13b2785/propcache-0.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e9b0d8d0845bbc4cfcdcbcdbf5086886bc8157aa963c31c777ceff7846c77757", size = 216727 }, + { url = "https://files.pythonhosted.org/packages/79/37/3ec3f7e3173e73f1d600495d8b545b53802cbf35506e5732dd8578db3724/propcache-0.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:981333cb2f4c1896a12f4ab92a9cc8f09ea664e9b7dbdc4eff74627af3a11c0f", size = 205097 }, + { url = "https://files.pythonhosted.org/packages/61/b0/b2631c19793f869d35f47d5a3a56fb19e9160d3c119f15ac7344fc3ccae7/propcache-0.4.1-cp311-cp311-win32.whl", hash = "sha256:f1d2f90aeec838a52f1c1a32fe9a619fefd5e411721a9117fbf82aea638fe8a1", size = 38084 }, + { url = "https://files.pythonhosted.org/packages/f4/78/6cce448e2098e9f3bfc91bb877f06aa24b6ccace872e39c53b2f707c4648/propcache-0.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:364426a62660f3f699949ac8c621aad6977be7126c5807ce48c0aeb8e7333ea6", size = 41637 }, + { url = "https://files.pythonhosted.org/packages/9c/e9/754f180cccd7f51a39913782c74717c581b9cc8177ad0e949f4d51812383/propcache-0.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:e53f3a38d3510c11953f3e6a33f205c6d1b001129f972805ca9b42fc308bc239", size = 38064 }, + { url = "https://files.pythonhosted.org/packages/a2/0f/f17b1b2b221d5ca28b4b876e8bb046ac40466513960646bda8e1853cdfa2/propcache-0.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e153e9cd40cc8945138822807139367f256f89c6810c2634a4f6902b52d3b4e2", size = 80061 }, + { url = "https://files.pythonhosted.org/packages/76/47/8ccf75935f51448ba9a16a71b783eb7ef6b9ee60f5d14c7f8a8a79fbeed7/propcache-0.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cd547953428f7abb73c5ad82cbb32109566204260d98e41e5dfdc682eb7f8403", size = 46037 }, + { url = "https://files.pythonhosted.org/packages/0a/b6/5c9a0e42df4d00bfb4a3cbbe5cf9f54260300c88a0e9af1f47ca5ce17ac0/propcache-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f048da1b4f243fc44f205dfd320933a951b8d89e0afd4c7cacc762a8b9165207", size = 47324 }, + { url = "https://files.pythonhosted.org/packages/9e/d3/6c7ee328b39a81ee877c962469f1e795f9db87f925251efeb0545e0020d0/propcache-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ec17c65562a827bba85e3872ead335f95405ea1674860d96483a02f5c698fa72", size = 225505 }, + { url = "https://files.pythonhosted.org/packages/01/5d/1c53f4563490b1d06a684742cc6076ef944bc6457df6051b7d1a877c057b/propcache-0.4.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:405aac25c6394ef275dee4c709be43745d36674b223ba4eb7144bf4d691b7367", size = 230242 }, + { url = "https://files.pythonhosted.org/packages/20/e1/ce4620633b0e2422207c3cb774a0ee61cac13abc6217763a7b9e2e3f4a12/propcache-0.4.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0013cb6f8dde4b2a2f66903b8ba740bdfe378c943c4377a200551ceb27f379e4", size = 238474 }, + { url = "https://files.pythonhosted.org/packages/46/4b/3aae6835b8e5f44ea6a68348ad90f78134047b503765087be2f9912140ea/propcache-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15932ab57837c3368b024473a525e25d316d8353016e7cc0e5ba9eb343fbb1cf", size = 221575 }, + { url = "https://files.pythonhosted.org/packages/6e/a5/8a5e8678bcc9d3a1a15b9a29165640d64762d424a16af543f00629c87338/propcache-0.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:031dce78b9dc099f4c29785d9cf5577a3faf9ebf74ecbd3c856a7b92768c3df3", size = 216736 }, + { url = "https://files.pythonhosted.org/packages/f1/63/b7b215eddeac83ca1c6b934f89d09a625aa9ee4ba158338854c87210cc36/propcache-0.4.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ab08df6c9a035bee56e31af99be621526bd237bea9f32def431c656b29e41778", size = 213019 }, + { url = "https://files.pythonhosted.org/packages/57/74/f580099a58c8af587cac7ba19ee7cb418506342fbbe2d4a4401661cca886/propcache-0.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4d7af63f9f93fe593afbf104c21b3b15868efb2c21d07d8732c0c4287e66b6a6", size = 220376 }, + { url = "https://files.pythonhosted.org/packages/c4/ee/542f1313aff7eaf19c2bb758c5d0560d2683dac001a1c96d0774af799843/propcache-0.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cfc27c945f422e8b5071b6e93169679e4eb5bf73bbcbf1ba3ae3a83d2f78ebd9", size = 226988 }, + { url = "https://files.pythonhosted.org/packages/8f/18/9c6b015dd9c6930f6ce2229e1f02fb35298b847f2087ea2b436a5bfa7287/propcache-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:35c3277624a080cc6ec6f847cbbbb5b49affa3598c4535a0a4682a697aaa5c75", size = 215615 }, + { url = "https://files.pythonhosted.org/packages/80/9e/e7b85720b98c45a45e1fca6a177024934dc9bc5f4d5dd04207f216fc33ed/propcache-0.4.1-cp312-cp312-win32.whl", hash = "sha256:671538c2262dadb5ba6395e26c1731e1d52534bfe9ae56d0b5573ce539266aa8", size = 38066 }, + { url = "https://files.pythonhosted.org/packages/54/09/d19cff2a5aaac632ec8fc03737b223597b1e347416934c1b3a7df079784c/propcache-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:cb2d222e72399fcf5890d1d5cc1060857b9b236adff2792ff48ca2dfd46c81db", size = 41655 }, + { url = "https://files.pythonhosted.org/packages/68/ab/6b5c191bb5de08036a8c697b265d4ca76148efb10fa162f14af14fb5f076/propcache-0.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:204483131fb222bdaaeeea9f9e6c6ed0cac32731f75dfc1d4a567fc1926477c1", size = 37789 }, + { url = "https://files.pythonhosted.org/packages/bf/df/6d9c1b6ac12b003837dde8a10231a7344512186e87b36e855bef32241942/propcache-0.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:43eedf29202c08550aac1d14e0ee619b0430aaef78f85864c1a892294fbc28cf", size = 77750 }, + { url = "https://files.pythonhosted.org/packages/8b/e8/677a0025e8a2acf07d3418a2e7ba529c9c33caf09d3c1f25513023c1db56/propcache-0.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d62cdfcfd89ccb8de04e0eda998535c406bf5e060ffd56be6c586cbcc05b3311", size = 44780 }, + { url = "https://files.pythonhosted.org/packages/89/a4/92380f7ca60f99ebae761936bc48a72a639e8a47b29050615eef757cb2a7/propcache-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cae65ad55793da34db5f54e4029b89d3b9b9490d8abe1b4c7ab5d4b8ec7ebf74", size = 46308 }, + { url = "https://files.pythonhosted.org/packages/2d/48/c5ac64dee5262044348d1d78a5f85dd1a57464a60d30daee946699963eb3/propcache-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:333ddb9031d2704a301ee3e506dc46b1fe5f294ec198ed6435ad5b6a085facfe", size = 208182 }, + { url = "https://files.pythonhosted.org/packages/c6/0c/cd762dd011a9287389a6a3eb43aa30207bde253610cca06824aeabfe9653/propcache-0.4.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fd0858c20f078a32cf55f7e81473d96dcf3b93fd2ccdb3d40fdf54b8573df3af", size = 211215 }, + { url = "https://files.pythonhosted.org/packages/30/3e/49861e90233ba36890ae0ca4c660e95df565b2cd15d4a68556ab5865974e/propcache-0.4.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:678ae89ebc632c5c204c794f8dab2837c5f159aeb59e6ed0539500400577298c", size = 218112 }, + { url = "https://files.pythonhosted.org/packages/f1/8b/544bc867e24e1bd48f3118cecd3b05c694e160a168478fa28770f22fd094/propcache-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d472aeb4fbf9865e0c6d622d7f4d54a4e101a89715d8904282bb5f9a2f476c3f", size = 204442 }, + { url = "https://files.pythonhosted.org/packages/50/a6/4282772fd016a76d3e5c0df58380a5ea64900afd836cec2c2f662d1b9bb3/propcache-0.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4d3df5fa7e36b3225954fba85589da77a0fe6a53e3976de39caf04a0db4c36f1", size = 199398 }, + { url = "https://files.pythonhosted.org/packages/3e/ec/d8a7cd406ee1ddb705db2139f8a10a8a427100347bd698e7014351c7af09/propcache-0.4.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ee17f18d2498f2673e432faaa71698032b0127ebf23ae5974eeaf806c279df24", size = 196920 }, + { url = "https://files.pythonhosted.org/packages/f6/6c/f38ab64af3764f431e359f8baf9e0a21013e24329e8b85d2da32e8ed07ca/propcache-0.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:580e97762b950f993ae618e167e7be9256b8353c2dcd8b99ec100eb50f5286aa", size = 203748 }, + { url = "https://files.pythonhosted.org/packages/d6/e3/fa846bd70f6534d647886621388f0a265254d30e3ce47e5c8e6e27dbf153/propcache-0.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:501d20b891688eb8e7aa903021f0b72d5a55db40ffaab27edefd1027caaafa61", size = 205877 }, + { url = "https://files.pythonhosted.org/packages/e2/39/8163fc6f3133fea7b5f2827e8eba2029a0277ab2c5beee6c1db7b10fc23d/propcache-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a0bd56e5b100aef69bd8562b74b46254e7c8812918d3baa700c8a8009b0af66", size = 199437 }, + { url = "https://files.pythonhosted.org/packages/93/89/caa9089970ca49c7c01662bd0eeedfe85494e863e8043565aeb6472ce8fe/propcache-0.4.1-cp313-cp313-win32.whl", hash = "sha256:bcc9aaa5d80322bc2fb24bb7accb4a30f81e90ab8d6ba187aec0744bc302ad81", size = 37586 }, + { url = "https://files.pythonhosted.org/packages/f5/ab/f76ec3c3627c883215b5c8080debb4394ef5a7a29be811f786415fc1e6fd/propcache-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:381914df18634f5494334d201e98245c0596067504b9372d8cf93f4bb23e025e", size = 40790 }, + { url = "https://files.pythonhosted.org/packages/59/1b/e71ae98235f8e2ba5004d8cb19765a74877abf189bc53fc0c80d799e56c3/propcache-0.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:8873eb4460fd55333ea49b7d189749ecf6e55bf85080f11b1c4530ed3034cba1", size = 37158 }, + { url = "https://files.pythonhosted.org/packages/83/ce/a31bbdfc24ee0dcbba458c8175ed26089cf109a55bbe7b7640ed2470cfe9/propcache-0.4.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:92d1935ee1f8d7442da9c0c4fa7ac20d07e94064184811b685f5c4fada64553b", size = 81451 }, + { url = "https://files.pythonhosted.org/packages/25/9c/442a45a470a68456e710d96cacd3573ef26a1d0a60067e6a7d5e655621ed/propcache-0.4.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:473c61b39e1460d386479b9b2f337da492042447c9b685f28be4f74d3529e566", size = 46374 }, + { url = "https://files.pythonhosted.org/packages/f4/bf/b1d5e21dbc3b2e889ea4327044fb16312a736d97640fb8b6aa3f9c7b3b65/propcache-0.4.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c0ef0aaafc66fbd87842a3fe3902fd889825646bc21149eafe47be6072725835", size = 48396 }, + { url = "https://files.pythonhosted.org/packages/f4/04/5b4c54a103d480e978d3c8a76073502b18db0c4bc17ab91b3cb5092ad949/propcache-0.4.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f95393b4d66bfae908c3ca8d169d5f79cd65636ae15b5e7a4f6e67af675adb0e", size = 275950 }, + { url = "https://files.pythonhosted.org/packages/b4/c1/86f846827fb969c4b78b0af79bba1d1ea2156492e1b83dea8b8a6ae27395/propcache-0.4.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c07fda85708bc48578467e85099645167a955ba093be0a2dcba962195676e859", size = 273856 }, + { url = "https://files.pythonhosted.org/packages/36/1d/fc272a63c8d3bbad6878c336c7a7dea15e8f2d23a544bda43205dfa83ada/propcache-0.4.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:af223b406d6d000830c6f65f1e6431783fc3f713ba3e6cc8c024d5ee96170a4b", size = 280420 }, + { url = "https://files.pythonhosted.org/packages/07/0c/01f2219d39f7e53d52e5173bcb09c976609ba30209912a0680adfb8c593a/propcache-0.4.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a78372c932c90ee474559c5ddfffd718238e8673c340dc21fe45c5b8b54559a0", size = 263254 }, + { url = "https://files.pythonhosted.org/packages/2d/18/cd28081658ce597898f0c4d174d4d0f3c5b6d4dc27ffafeef835c95eb359/propcache-0.4.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:564d9f0d4d9509e1a870c920a89b2fec951b44bf5ba7d537a9e7c1ccec2c18af", size = 261205 }, + { url = "https://files.pythonhosted.org/packages/7a/71/1f9e22eb8b8316701c2a19fa1f388c8a3185082607da8e406a803c9b954e/propcache-0.4.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:17612831fda0138059cc5546f4d12a2aacfb9e47068c06af35c400ba58ba7393", size = 247873 }, + { url = "https://files.pythonhosted.org/packages/4a/65/3d4b61f36af2b4eddba9def857959f1016a51066b4f1ce348e0cf7881f58/propcache-0.4.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:41a89040cb10bd345b3c1a873b2bf36413d48da1def52f268a055f7398514874", size = 262739 }, + { url = "https://files.pythonhosted.org/packages/2a/42/26746ab087faa77c1c68079b228810436ccd9a5ce9ac85e2b7307195fd06/propcache-0.4.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e35b88984e7fa64aacecea39236cee32dd9bd8c55f57ba8a75cf2399553f9bd7", size = 263514 }, + { url = "https://files.pythonhosted.org/packages/94/13/630690fe201f5502d2403dd3cfd451ed8858fe3c738ee88d095ad2ff407b/propcache-0.4.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f8b465489f927b0df505cbe26ffbeed4d6d8a2bbc61ce90eb074ff129ef0ab1", size = 257781 }, + { url = "https://files.pythonhosted.org/packages/92/f7/1d4ec5841505f423469efbfc381d64b7b467438cd5a4bbcbb063f3b73d27/propcache-0.4.1-cp313-cp313t-win32.whl", hash = "sha256:2ad890caa1d928c7c2965b48f3a3815c853180831d0e5503d35cf00c472f4717", size = 41396 }, + { url = "https://files.pythonhosted.org/packages/48/f0/615c30622316496d2cbbc29f5985f7777d3ada70f23370608c1d3e081c1f/propcache-0.4.1-cp313-cp313t-win_amd64.whl", hash = "sha256:f7ee0e597f495cf415bcbd3da3caa3bd7e816b74d0d52b8145954c5e6fd3ff37", size = 44897 }, + { url = "https://files.pythonhosted.org/packages/fd/ca/6002e46eccbe0e33dcd4069ef32f7f1c9e243736e07adca37ae8c4830ec3/propcache-0.4.1-cp313-cp313t-win_arm64.whl", hash = "sha256:929d7cbe1f01bb7baffb33dc14eb5691c95831450a26354cd210a8155170c93a", size = 39789 }, + { url = "https://files.pythonhosted.org/packages/8e/5c/bca52d654a896f831b8256683457ceddd490ec18d9ec50e97dfd8fc726a8/propcache-0.4.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3f7124c9d820ba5548d431afb4632301acf965db49e666aa21c305cbe8c6de12", size = 78152 }, + { url = "https://files.pythonhosted.org/packages/65/9b/03b04e7d82a5f54fb16113d839f5ea1ede58a61e90edf515f6577c66fa8f/propcache-0.4.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c0d4b719b7da33599dfe3b22d3db1ef789210a0597bc650b7cee9c77c2be8c5c", size = 44869 }, + { url = "https://files.pythonhosted.org/packages/b2/fa/89a8ef0468d5833a23fff277b143d0573897cf75bd56670a6d28126c7d68/propcache-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f302f4783709a78240ebc311b793f123328716a60911d667e0c036bc5dcbded", size = 46596 }, + { url = "https://files.pythonhosted.org/packages/86/bd/47816020d337f4a746edc42fe8d53669965138f39ee117414c7d7a340cfe/propcache-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c80ee5802e3fb9ea37938e7eecc307fb984837091d5fd262bb37238b1ae97641", size = 206981 }, + { url = "https://files.pythonhosted.org/packages/df/f6/c5fa1357cc9748510ee55f37173eb31bfde6d94e98ccd9e6f033f2fc06e1/propcache-0.4.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ed5a841e8bb29a55fb8159ed526b26adc5bdd7e8bd7bf793ce647cb08656cdf4", size = 211490 }, + { url = "https://files.pythonhosted.org/packages/80/1e/e5889652a7c4a3846683401a48f0f2e5083ce0ec1a8a5221d8058fbd1adf/propcache-0.4.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:55c72fd6ea2da4c318e74ffdf93c4fe4e926051133657459131a95c846d16d44", size = 215371 }, + { url = "https://files.pythonhosted.org/packages/b2/f2/889ad4b2408f72fe1a4f6a19491177b30ea7bf1a0fd5f17050ca08cfc882/propcache-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8326e144341460402713f91df60ade3c999d601e7eb5ff8f6f7862d54de0610d", size = 201424 }, + { url = "https://files.pythonhosted.org/packages/27/73/033d63069b57b0812c8bd19f311faebeceb6ba31b8f32b73432d12a0b826/propcache-0.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:060b16ae65bc098da7f6d25bf359f1f31f688384858204fe5d652979e0015e5b", size = 197566 }, + { url = "https://files.pythonhosted.org/packages/dc/89/ce24f3dc182630b4e07aa6d15f0ff4b14ed4b9955fae95a0b54c58d66c05/propcache-0.4.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:89eb3fa9524f7bec9de6e83cf3faed9d79bffa560672c118a96a171a6f55831e", size = 193130 }, + { url = "https://files.pythonhosted.org/packages/a9/24/ef0d5fd1a811fb5c609278d0209c9f10c35f20581fcc16f818da959fc5b4/propcache-0.4.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:dee69d7015dc235f526fe80a9c90d65eb0039103fe565776250881731f06349f", size = 202625 }, + { url = "https://files.pythonhosted.org/packages/f5/02/98ec20ff5546f68d673df2f7a69e8c0d076b5abd05ca882dc7ee3a83653d/propcache-0.4.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5558992a00dfd54ccbc64a32726a3357ec93825a418a401f5cc67df0ac5d9e49", size = 204209 }, + { url = "https://files.pythonhosted.org/packages/a0/87/492694f76759b15f0467a2a93ab68d32859672b646aa8a04ce4864e7932d/propcache-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c9b822a577f560fbd9554812526831712c1436d2c046cedee4c3796d3543b144", size = 197797 }, + { url = "https://files.pythonhosted.org/packages/ee/36/66367de3575db1d2d3f3d177432bd14ee577a39d3f5d1b3d5df8afe3b6e2/propcache-0.4.1-cp314-cp314-win32.whl", hash = "sha256:ab4c29b49d560fe48b696cdcb127dd36e0bc2472548f3bf56cc5cb3da2b2984f", size = 38140 }, + { url = "https://files.pythonhosted.org/packages/0c/2a/a758b47de253636e1b8aef181c0b4f4f204bf0dd964914fb2af90a95b49b/propcache-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:5a103c3eb905fcea0ab98be99c3a9a5ab2de60228aa5aceedc614c0281cf6153", size = 41257 }, + { url = "https://files.pythonhosted.org/packages/34/5e/63bd5896c3fec12edcbd6f12508d4890d23c265df28c74b175e1ef9f4f3b/propcache-0.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:74c1fb26515153e482e00177a1ad654721bf9207da8a494a0c05e797ad27b992", size = 38097 }, + { url = "https://files.pythonhosted.org/packages/99/85/9ff785d787ccf9bbb3f3106f79884a130951436f58392000231b4c737c80/propcache-0.4.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:824e908bce90fb2743bd6b59db36eb4f45cd350a39637c9f73b1c1ea66f5b75f", size = 81455 }, + { url = "https://files.pythonhosted.org/packages/90/85/2431c10c8e7ddb1445c1f7c4b54d886e8ad20e3c6307e7218f05922cad67/propcache-0.4.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2b5e7db5328427c57c8e8831abda175421b709672f6cfc3d630c3b7e2146393", size = 46372 }, + { url = "https://files.pythonhosted.org/packages/01/20/b0972d902472da9bcb683fa595099911f4d2e86e5683bcc45de60dd05dc3/propcache-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6f6ff873ed40292cd4969ef5310179afd5db59fdf055897e282485043fc80ad0", size = 48411 }, + { url = "https://files.pythonhosted.org/packages/e2/e3/7dc89f4f21e8f99bad3d5ddb3a3389afcf9da4ac69e3deb2dcdc96e74169/propcache-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49a2dc67c154db2c1463013594c458881a069fcf98940e61a0569016a583020a", size = 275712 }, + { url = "https://files.pythonhosted.org/packages/20/67/89800c8352489b21a8047c773067644e3897f02ecbbd610f4d46b7f08612/propcache-0.4.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:005f08e6a0529984491e37d8dbc3dd86f84bd78a8ceb5fa9a021f4c48d4984be", size = 273557 }, + { url = "https://files.pythonhosted.org/packages/e2/a1/b52b055c766a54ce6d9c16d9aca0cad8059acd9637cdf8aa0222f4a026ef/propcache-0.4.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5c3310452e0d31390da9035c348633b43d7e7feb2e37be252be6da45abd1abcc", size = 280015 }, + { url = "https://files.pythonhosted.org/packages/48/c8/33cee30bd890672c63743049f3c9e4be087e6780906bfc3ec58528be59c1/propcache-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c3c70630930447f9ef1caac7728c8ad1c56bc5015338b20fed0d08ea2480b3a", size = 262880 }, + { url = "https://files.pythonhosted.org/packages/0c/b1/8f08a143b204b418285c88b83d00edbd61afbc2c6415ffafc8905da7038b/propcache-0.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e57061305815dfc910a3634dcf584f08168a8836e6999983569f51a8544cd89", size = 260938 }, + { url = "https://files.pythonhosted.org/packages/cf/12/96e4664c82ca2f31e1c8dff86afb867348979eb78d3cb8546a680287a1e9/propcache-0.4.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:521a463429ef54143092c11a77e04056dd00636f72e8c45b70aaa3140d639726", size = 247641 }, + { url = "https://files.pythonhosted.org/packages/18/ed/e7a9cfca28133386ba52278136d42209d3125db08d0a6395f0cba0c0285c/propcache-0.4.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:120c964da3fdc75e3731aa392527136d4ad35868cc556fd09bb6d09172d9a367", size = 262510 }, + { url = "https://files.pythonhosted.org/packages/f5/76/16d8bf65e8845dd62b4e2b57444ab81f07f40caa5652b8969b87ddcf2ef6/propcache-0.4.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:d8f353eb14ee3441ee844ade4277d560cdd68288838673273b978e3d6d2c8f36", size = 263161 }, + { url = "https://files.pythonhosted.org/packages/e7/70/c99e9edb5d91d5ad8a49fa3c1e8285ba64f1476782fed10ab251ff413ba1/propcache-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ab2943be7c652f09638800905ee1bab2c544e537edb57d527997a24c13dc1455", size = 257393 }, + { url = "https://files.pythonhosted.org/packages/08/02/87b25304249a35c0915d236575bc3574a323f60b47939a2262b77632a3ee/propcache-0.4.1-cp314-cp314t-win32.whl", hash = "sha256:05674a162469f31358c30bcaa8883cb7829fa3110bf9c0991fe27d7896c42d85", size = 42546 }, + { url = "https://files.pythonhosted.org/packages/cb/ef/3c6ecf8b317aa982f309835e8f96987466123c6e596646d4e6a1dfcd080f/propcache-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:990f6b3e2a27d683cb7602ed6c86f15ee6b43b1194736f9baaeb93d0016633b1", size = 46259 }, + { url = "https://files.pythonhosted.org/packages/c4/2d/346e946d4951f37eca1e4f55be0f0174c52cd70720f84029b02f296f4a38/propcache-0.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ecef2343af4cc68e05131e45024ba34f6095821988a9d0a02aa7c73fcc448aa9", size = 40428 }, + { url = "https://files.pythonhosted.org/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", size = 13305 }, +] + +[[package]] +name = "pycparser" +version = "3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172 }, +] + +[[package]] +name = "pydantic" +version = "2.12.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580 }, +] + +[[package]] +name = "pydantic-core" +version = "2.41.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/90/32c9941e728d564b411d574d8ee0cf09b12ec978cb22b294995bae5549a5/pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146", size = 2107298 }, + { url = "https://files.pythonhosted.org/packages/fb/a8/61c96a77fe28993d9a6fb0f4127e05430a267b235a124545d79fea46dd65/pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2", size = 1901475 }, + { url = "https://files.pythonhosted.org/packages/5d/b6/338abf60225acc18cdc08b4faef592d0310923d19a87fba1faf05af5346e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97", size = 1918815 }, + { url = "https://files.pythonhosted.org/packages/d1/1c/2ed0433e682983d8e8cba9c8d8ef274d4791ec6a6f24c58935b90e780e0a/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9", size = 2065567 }, + { url = "https://files.pythonhosted.org/packages/b3/24/cf84974ee7d6eae06b9e63289b7b8f6549d416b5c199ca2d7ce13bbcf619/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52", size = 2230442 }, + { url = "https://files.pythonhosted.org/packages/fd/21/4e287865504b3edc0136c89c9c09431be326168b1eb7841911cbc877a995/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941", size = 2350956 }, + { url = "https://files.pythonhosted.org/packages/a8/76/7727ef2ffa4b62fcab916686a68a0426b9b790139720e1934e8ba797e238/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a", size = 2068253 }, + { url = "https://files.pythonhosted.org/packages/d5/8c/a4abfc79604bcb4c748e18975c44f94f756f08fb04218d5cb87eb0d3a63e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c", size = 2177050 }, + { url = "https://files.pythonhosted.org/packages/67/b1/de2e9a9a79b480f9cb0b6e8b6ba4c50b18d4e89852426364c66aa82bb7b3/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2", size = 2147178 }, + { url = "https://files.pythonhosted.org/packages/16/c1/dfb33f837a47b20417500efaa0378adc6635b3c79e8369ff7a03c494b4ac/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556", size = 2341833 }, + { url = "https://files.pythonhosted.org/packages/47/36/00f398642a0f4b815a9a558c4f1dca1b4020a7d49562807d7bc9ff279a6c/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49", size = 2321156 }, + { url = "https://files.pythonhosted.org/packages/7e/70/cad3acd89fde2010807354d978725ae111ddf6d0ea46d1ea1775b5c1bd0c/pydantic_core-2.41.5-cp310-cp310-win32.whl", hash = "sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba", size = 1989378 }, + { url = "https://files.pythonhosted.org/packages/76/92/d338652464c6c367e5608e4488201702cd1cbb0f33f7b6a85a60fe5f3720/pydantic_core-2.41.5-cp310-cp310-win_amd64.whl", hash = "sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9", size = 2013622 }, + { url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873 }, + { url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826 }, + { url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869 }, + { url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890 }, + { url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740 }, + { url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021 }, + { url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378 }, + { url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761 }, + { url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303 }, + { url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355 }, + { url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875 }, + { url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549 }, + { url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305 }, + { url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902 }, + { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990 }, + { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003 }, + { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200 }, + { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578 }, + { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504 }, + { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816 }, + { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366 }, + { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698 }, + { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603 }, + { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591 }, + { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068 }, + { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908 }, + { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145 }, + { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179 }, + { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403 }, + { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206 }, + { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307 }, + { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258 }, + { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917 }, + { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186 }, + { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164 }, + { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146 }, + { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788 }, + { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133 }, + { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852 }, + { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679 }, + { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766 }, + { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005 }, + { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622 }, + { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725 }, + { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040 }, + { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691 }, + { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897 }, + { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302 }, + { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877 }, + { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680 }, + { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960 }, + { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102 }, + { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039 }, + { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126 }, + { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489 }, + { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288 }, + { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255 }, + { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760 }, + { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092 }, + { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385 }, + { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832 }, + { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585 }, + { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078 }, + { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914 }, + { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560 }, + { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244 }, + { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955 }, + { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906 }, + { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607 }, + { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769 }, + { url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441 }, + { url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291 }, + { url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632 }, + { url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905 }, + { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495 }, + { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388 }, + { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879 }, + { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017 }, + { url = "https://files.pythonhosted.org/packages/e6/b0/1a2aa41e3b5a4ba11420aba2d091b2d17959c8d1519ece3627c371951e73/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8", size = 2103351 }, + { url = "https://files.pythonhosted.org/packages/a4/ee/31b1f0020baaf6d091c87900ae05c6aeae101fa4e188e1613c80e4f1ea31/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a", size = 1925363 }, + { url = "https://files.pythonhosted.org/packages/e1/89/ab8e86208467e467a80deaca4e434adac37b10a9d134cd2f99b28a01e483/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b", size = 2135615 }, + { url = "https://files.pythonhosted.org/packages/99/0a/99a53d06dd0348b2008f2f30884b34719c323f16c3be4e6cc1203b74a91d/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2", size = 2175369 }, + { url = "https://files.pythonhosted.org/packages/6d/94/30ca3b73c6d485b9bb0bc66e611cff4a7138ff9736b7e66bcf0852151636/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093", size = 2144218 }, + { url = "https://files.pythonhosted.org/packages/87/57/31b4f8e12680b739a91f472b5671294236b82586889ef764b5fbc6669238/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a", size = 2329951 }, + { url = "https://files.pythonhosted.org/packages/7d/73/3c2c8edef77b8f7310e6fb012dbc4b8551386ed575b9eb6fb2506e28a7eb/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963", size = 2318428 }, + { url = "https://files.pythonhosted.org/packages/2f/02/8559b1f26ee0d502c74f9cca5c0d2fd97e967e083e006bbbb4e97f3a043a/pydantic_core-2.41.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a", size = 2147009 }, + { url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980 }, + { url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865 }, + { url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256 }, + { url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762 }, + { url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141 }, + { url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317 }, + { url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992 }, + { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302 }, +] + +[[package]] +name = "python-dotenv" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101 }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227 }, + { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019 }, + { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646 }, + { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793 }, + { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293 }, + { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872 }, + { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828 }, + { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415 }, + { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561 }, + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826 }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577 }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556 }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114 }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638 }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463 }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986 }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543 }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763 }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063 }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973 }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116 }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011 }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870 }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089 }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181 }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658 }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003 }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344 }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669 }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252 }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081 }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159 }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626 }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613 }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115 }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427 }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090 }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246 }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814 }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809 }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454 }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355 }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175 }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228 }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194 }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429 }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912 }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108 }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641 }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901 }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132 }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261 }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272 }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923 }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062 }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341 }, +] + +[[package]] +name = "requests" +version = "2.32.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738 }, +] + +[[package]] +name = "starlette" +version = "0.52.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c4/68/79977123bb7be889ad680d79a40f339082c1978b5cfcf62c2d8d196873ac/starlette-0.52.1.tar.gz", hash = "sha256:834edd1b0a23167694292e94f597773bc3f89f362be6effee198165a35d62933", size = 2653702 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/0d/13d1d239a25cbfb19e740db83143e95c772a1fe10202dda4b76792b114dd/starlette-0.52.1-py3-none-any.whl", hash = "sha256:0029d43eb3d273bc4f83a08720b4912ea4b071087a3b48db01b7c839f7954d74", size = 74272 }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614 }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611 }, +] + +[[package]] +name = "urllib3" +version = "2.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584 }, +] + +[[package]] +name = "uvicorn" +version = "0.41.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/32/ce/eeb58ae4ac36fe09e3842eb02e0eb676bf2c53ae062b98f1b2531673efdd/uvicorn-0.41.0.tar.gz", hash = "sha256:09d11cf7008da33113824ee5a1c6422d89fbc2ff476540d69a34c87fab8b571a", size = 82633 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/e4/d04a086285c20886c0daad0e026f250869201013d18f81d9ff5eada73a88/uvicorn-0.41.0-py3-none-any.whl", hash = "sha256:29e35b1d2c36a04b9e180d4007ede3bcb32a85fbdfd6c6aeb3f26839de088187", size = 68783 }, +] + +[package.optional-dependencies] +standard = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "httptools" }, + { name = "python-dotenv" }, + { name = "pyyaml" }, + { name = "uvloop", marker = "platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'" }, + { name = "watchfiles" }, + { name = "websockets" }, +] + +[[package]] +name = "uvloop" +version = "0.22.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/06/f0/18d39dbd1971d6d62c4629cc7fa67f74821b0dc1f5a77af43719de7936a7/uvloop-0.22.1.tar.gz", hash = "sha256:6c84bae345b9147082b17371e3dd5d42775bddce91f885499017f4607fdaf39f", size = 2443250 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/14/ecceb239b65adaaf7fde510aa8bd534075695d1e5f8dadfa32b5723d9cfb/uvloop-0.22.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ef6f0d4cc8a9fa1f6a910230cd53545d9a14479311e87e3cb225495952eb672c", size = 1343335 }, + { url = "https://files.pythonhosted.org/packages/ba/ae/6f6f9af7f590b319c94532b9567409ba11f4fa71af1148cab1bf48a07048/uvloop-0.22.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7cd375a12b71d33d46af85a3343b35d98e8116134ba404bd657b3b1d15988792", size = 742903 }, + { url = "https://files.pythonhosted.org/packages/09/bd/3667151ad0702282a1f4d5d29288fce8a13c8b6858bf0978c219cd52b231/uvloop-0.22.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac33ed96229b7790eb729702751c0e93ac5bc3bcf52ae9eccbff30da09194b86", size = 3648499 }, + { url = "https://files.pythonhosted.org/packages/b3/f6/21657bb3beb5f8c57ce8be3b83f653dd7933c2fd00545ed1b092d464799a/uvloop-0.22.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:481c990a7abe2c6f4fc3d98781cc9426ebd7f03a9aaa7eb03d3bfc68ac2a46bd", size = 3700133 }, + { url = "https://files.pythonhosted.org/packages/09/e0/604f61d004ded805f24974c87ddd8374ef675644f476f01f1df90e4cdf72/uvloop-0.22.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a592b043a47ad17911add5fbd087c76716d7c9ccc1d64ec9249ceafd735f03c2", size = 3512681 }, + { url = "https://files.pythonhosted.org/packages/bb/ce/8491fd370b0230deb5eac69c7aae35b3be527e25a911c0acdffb922dc1cd/uvloop-0.22.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1489cf791aa7b6e8c8be1c5a080bae3a672791fcb4e9e12249b05862a2ca9cec", size = 3615261 }, + { url = "https://files.pythonhosted.org/packages/c7/d5/69900f7883235562f1f50d8184bb7dd84a2fb61e9ec63f3782546fdbd057/uvloop-0.22.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c60ebcd36f7b240b30788554b6f0782454826a0ed765d8430652621b5de674b9", size = 1352420 }, + { url = "https://files.pythonhosted.org/packages/a8/73/c4e271b3bce59724e291465cc936c37758886a4868787da0278b3b56b905/uvloop-0.22.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3b7f102bf3cb1995cfeaee9321105e8f5da76fdb104cdad8986f85461a1b7b77", size = 748677 }, + { url = "https://files.pythonhosted.org/packages/86/94/9fb7fad2f824d25f8ecac0d70b94d0d48107ad5ece03769a9c543444f78a/uvloop-0.22.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53c85520781d84a4b8b230e24a5af5b0778efdb39142b424990ff1ef7c48ba21", size = 3753819 }, + { url = "https://files.pythonhosted.org/packages/74/4f/256aca690709e9b008b7108bc85fba619a2bc37c6d80743d18abad16ee09/uvloop-0.22.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:56a2d1fae65fd82197cb8c53c367310b3eabe1bbb9fb5a04d28e3e3520e4f702", size = 3804529 }, + { url = "https://files.pythonhosted.org/packages/7f/74/03c05ae4737e871923d21a76fe28b6aad57f5c03b6e6bfcfa5ad616013e4/uvloop-0.22.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:40631b049d5972c6755b06d0bfe8233b1bd9a8a6392d9d1c45c10b6f9e9b2733", size = 3621267 }, + { url = "https://files.pythonhosted.org/packages/75/be/f8e590fe61d18b4a92070905497aec4c0e64ae1761498cad09023f3f4b3e/uvloop-0.22.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:535cc37b3a04f6cd2c1ef65fa1d370c9a35b6695df735fcff5427323f2cd5473", size = 3723105 }, + { url = "https://files.pythonhosted.org/packages/3d/ff/7f72e8170be527b4977b033239a83a68d5c881cc4775fca255c677f7ac5d/uvloop-0.22.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fe94b4564e865d968414598eea1a6de60adba0c040ba4ed05ac1300de402cd42", size = 1359936 }, + { url = "https://files.pythonhosted.org/packages/c3/c6/e5d433f88fd54d81ef4be58b2b7b0cea13c442454a1db703a1eea0db1a59/uvloop-0.22.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:51eb9bd88391483410daad430813d982010f9c9c89512321f5b60e2cddbdddd6", size = 752769 }, + { url = "https://files.pythonhosted.org/packages/24/68/a6ac446820273e71aa762fa21cdcc09861edd3536ff47c5cd3b7afb10eeb/uvloop-0.22.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:700e674a166ca5778255e0e1dc4e9d79ab2acc57b9171b79e65feba7184b3370", size = 4317413 }, + { url = "https://files.pythonhosted.org/packages/5f/6f/e62b4dfc7ad6518e7eff2516f680d02a0f6eb62c0c212e152ca708a0085e/uvloop-0.22.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b5b1ac819a3f946d3b2ee07f09149578ae76066d70b44df3fa990add49a82e4", size = 4426307 }, + { url = "https://files.pythonhosted.org/packages/90/60/97362554ac21e20e81bcef1150cb2a7e4ffdaf8ea1e5b2e8bf7a053caa18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e047cc068570bac9866237739607d1313b9253c3051ad84738cbb095be0537b2", size = 4131970 }, + { url = "https://files.pythonhosted.org/packages/99/39/6b3f7d234ba3964c428a6e40006340f53ba37993f46ed6e111c6e9141d18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:512fec6815e2dd45161054592441ef76c830eddaad55c8aa30952e6fe1ed07c0", size = 4296343 }, + { url = "https://files.pythonhosted.org/packages/89/8c/182a2a593195bfd39842ea68ebc084e20c850806117213f5a299dfc513d9/uvloop-0.22.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:561577354eb94200d75aca23fbde86ee11be36b00e52a4eaf8f50fb0c86b7705", size = 1358611 }, + { url = "https://files.pythonhosted.org/packages/d2/14/e301ee96a6dc95224b6f1162cd3312f6d1217be3907b79173b06785f2fe7/uvloop-0.22.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cdf5192ab3e674ca26da2eada35b288d2fa49fdd0f357a19f0e7c4e7d5077c8", size = 751811 }, + { url = "https://files.pythonhosted.org/packages/b7/02/654426ce265ac19e2980bfd9ea6590ca96a56f10c76e63801a2df01c0486/uvloop-0.22.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e2ea3d6190a2968f4a14a23019d3b16870dd2190cd69c8180f7c632d21de68d", size = 4288562 }, + { url = "https://files.pythonhosted.org/packages/15/c0/0be24758891ef825f2065cd5db8741aaddabe3e248ee6acc5e8a80f04005/uvloop-0.22.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0530a5fbad9c9e4ee3f2b33b148c6a64d47bbad8000ea63704fa8260f4cf728e", size = 4366890 }, + { url = "https://files.pythonhosted.org/packages/d2/53/8369e5219a5855869bcee5f4d317f6da0e2c669aecf0ef7d371e3d084449/uvloop-0.22.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bc5ef13bbc10b5335792360623cc378d52d7e62c2de64660616478c32cd0598e", size = 4119472 }, + { url = "https://files.pythonhosted.org/packages/f8/ba/d69adbe699b768f6b29a5eec7b47dd610bd17a69de51b251126a801369ea/uvloop-0.22.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1f38ec5e3f18c8a10ded09742f7fb8de0108796eb673f30ce7762ce1b8550cad", size = 4239051 }, + { url = "https://files.pythonhosted.org/packages/90/cd/b62bdeaa429758aee8de8b00ac0dd26593a9de93d302bff3d21439e9791d/uvloop-0.22.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3879b88423ec7e97cd4eba2a443aa26ed4e59b45e6b76aabf13fe2f27023a142", size = 1362067 }, + { url = "https://files.pythonhosted.org/packages/0d/f8/a132124dfda0777e489ca86732e85e69afcd1ff7686647000050ba670689/uvloop-0.22.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4baa86acedf1d62115c1dc6ad1e17134476688f08c6efd8a2ab076e815665c74", size = 752423 }, + { url = "https://files.pythonhosted.org/packages/a3/94/94af78c156f88da4b3a733773ad5ba0b164393e357cc4bd0ab2e2677a7d6/uvloop-0.22.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:297c27d8003520596236bdb2335e6b3f649480bd09e00d1e3a99144b691d2a35", size = 4272437 }, + { url = "https://files.pythonhosted.org/packages/b5/35/60249e9fd07b32c665192cec7af29e06c7cd96fa1d08b84f012a56a0b38e/uvloop-0.22.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1955d5a1dd43198244d47664a5858082a3239766a839b2102a269aaff7a4e25", size = 4292101 }, + { url = "https://files.pythonhosted.org/packages/02/62/67d382dfcb25d0a98ce73c11ed1a6fba5037a1a1d533dcbb7cab033a2636/uvloop-0.22.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b31dc2fccbd42adc73bc4e7cdbae4fc5086cf378979e53ca5d0301838c5682c6", size = 4114158 }, + { url = "https://files.pythonhosted.org/packages/f0/7a/f1171b4a882a5d13c8b7576f348acfe6074d72eaf52cccef752f748d4a9f/uvloop-0.22.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:93f617675b2d03af4e72a5333ef89450dfaa5321303ede6e67ba9c9d26878079", size = 4177360 }, + { url = "https://files.pythonhosted.org/packages/79/7b/b01414f31546caf0919da80ad57cbfe24c56b151d12af68cee1b04922ca8/uvloop-0.22.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:37554f70528f60cad66945b885eb01f1bb514f132d92b6eeed1c90fd54ed6289", size = 1454790 }, + { url = "https://files.pythonhosted.org/packages/d4/31/0bb232318dd838cad3fa8fb0c68c8b40e1145b32025581975e18b11fab40/uvloop-0.22.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b76324e2dc033a0b2f435f33eb88ff9913c156ef78e153fb210e03c13da746b3", size = 796783 }, + { url = "https://files.pythonhosted.org/packages/42/38/c9b09f3271a7a723a5de69f8e237ab8e7803183131bc57c890db0b6bb872/uvloop-0.22.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:badb4d8e58ee08dad957002027830d5c3b06aea446a6a3744483c2b3b745345c", size = 4647548 }, + { url = "https://files.pythonhosted.org/packages/c1/37/945b4ca0ac27e3dc4952642d4c900edd030b3da6c9634875af6e13ae80e5/uvloop-0.22.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b91328c72635f6f9e0282e4a57da7470c7350ab1c9f48546c0f2866205349d21", size = 4467065 }, + { url = "https://files.pythonhosted.org/packages/97/cc/48d232f33d60e2e2e0b42f4e73455b146b76ebe216487e862700457fbf3c/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:daf620c2995d193449393d6c62131b3fbd40a63bf7b307a1527856ace637fe88", size = 4328384 }, + { url = "https://files.pythonhosted.org/packages/e4/16/c1fd27e9549f3c4baf1dc9c20c456cd2f822dbf8de9f463824b0c0357e06/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6cde23eeda1a25c75b2e07d39970f3374105d5eafbaab2a4482be82f272d5a5e", size = 4296730 }, +] + +[[package]] +name = "watchfiles" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c2/c9/8869df9b2a2d6c59d79220a4db37679e74f807c559ffe5265e08b227a210/watchfiles-1.1.1.tar.gz", hash = "sha256:a173cb5c16c4f40ab19cecf48a534c409f7ea983ab8fed0741304a1c0a31b3f2", size = 94440 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/1a/206e8cf2dd86fddf939165a57b4df61607a1e0add2785f170a3f616b7d9f/watchfiles-1.1.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:eef58232d32daf2ac67f42dea51a2c80f0d03379075d44a587051e63cc2e368c", size = 407318 }, + { url = "https://files.pythonhosted.org/packages/b3/0f/abaf5262b9c496b5dad4ed3c0e799cbecb1f8ea512ecb6ddd46646a9fca3/watchfiles-1.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:03fa0f5237118a0c5e496185cafa92878568b652a2e9a9382a5151b1a0380a43", size = 394478 }, + { url = "https://files.pythonhosted.org/packages/b1/04/9cc0ba88697b34b755371f5ace8d3a4d9a15719c07bdc7bd13d7d8c6a341/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8ca65483439f9c791897f7db49202301deb6e15fe9f8fe2fed555bf986d10c31", size = 449894 }, + { url = "https://files.pythonhosted.org/packages/d2/9c/eda4615863cd8621e89aed4df680d8c3ec3da6a4cf1da113c17decd87c7f/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f0ab1c1af0cb38e3f598244c17919fb1a84d1629cc08355b0074b6d7f53138ac", size = 459065 }, + { url = "https://files.pythonhosted.org/packages/84/13/f28b3f340157d03cbc8197629bc109d1098764abe1e60874622a0be5c112/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3bc570d6c01c206c46deb6e935a260be44f186a2f05179f52f7fcd2be086a94d", size = 488377 }, + { url = "https://files.pythonhosted.org/packages/86/93/cfa597fa9389e122488f7ffdbd6db505b3b915ca7435ecd7542e855898c2/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e84087b432b6ac94778de547e08611266f1f8ffad28c0ee4c82e028b0fc5966d", size = 595837 }, + { url = "https://files.pythonhosted.org/packages/57/1e/68c1ed5652b48d89fc24d6af905d88ee4f82fa8bc491e2666004e307ded1/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:620bae625f4cb18427b1bb1a2d9426dc0dd5a5ba74c7c2cdb9de405f7b129863", size = 473456 }, + { url = "https://files.pythonhosted.org/packages/d5/dc/1a680b7458ffa3b14bb64878112aefc8f2e4f73c5af763cbf0bd43100658/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:544364b2b51a9b0c7000a4b4b02f90e9423d97fbbf7e06689236443ebcad81ab", size = 455614 }, + { url = "https://files.pythonhosted.org/packages/61/a5/3d782a666512e01eaa6541a72ebac1d3aae191ff4a31274a66b8dd85760c/watchfiles-1.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:bbe1ef33d45bc71cf21364df962af171f96ecaeca06bd9e3d0b583efb12aec82", size = 630690 }, + { url = "https://files.pythonhosted.org/packages/9b/73/bb5f38590e34687b2a9c47a244aa4dd50c56a825969c92c9c5fc7387cea1/watchfiles-1.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1a0bb430adb19ef49389e1ad368450193a90038b5b752f4ac089ec6942c4dff4", size = 622459 }, + { url = "https://files.pythonhosted.org/packages/f1/ac/c9bb0ec696e07a20bd58af5399aeadaef195fb2c73d26baf55180fe4a942/watchfiles-1.1.1-cp310-cp310-win32.whl", hash = "sha256:3f6d37644155fb5beca5378feb8c1708d5783145f2a0f1c4d5a061a210254844", size = 272663 }, + { url = "https://files.pythonhosted.org/packages/11/a0/a60c5a7c2ec59fa062d9a9c61d02e3b6abd94d32aac2d8344c4bdd033326/watchfiles-1.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:a36d8efe0f290835fd0f33da35042a1bb5dc0e83cbc092dcf69bce442579e88e", size = 287453 }, + { url = "https://files.pythonhosted.org/packages/1f/f8/2c5f479fb531ce2f0564eda479faecf253d886b1ab3630a39b7bf7362d46/watchfiles-1.1.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:f57b396167a2565a4e8b5e56a5a1c537571733992b226f4f1197d79e94cf0ae5", size = 406529 }, + { url = "https://files.pythonhosted.org/packages/fe/cd/f515660b1f32f65df671ddf6f85bfaca621aee177712874dc30a97397977/watchfiles-1.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:421e29339983e1bebc281fab40d812742268ad057db4aee8c4d2bce0af43b741", size = 394384 }, + { url = "https://files.pythonhosted.org/packages/7b/c3/28b7dc99733eab43fca2d10f55c86e03bd6ab11ca31b802abac26b23d161/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e43d39a741e972bab5d8100b5cdacf69db64e34eb19b6e9af162bccf63c5cc6", size = 448789 }, + { url = "https://files.pythonhosted.org/packages/4a/24/33e71113b320030011c8e4316ccca04194bf0cbbaeee207f00cbc7d6b9f5/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f537afb3276d12814082a2e9b242bdcf416c2e8fd9f799a737990a1dbe906e5b", size = 460521 }, + { url = "https://files.pythonhosted.org/packages/f4/c3/3c9a55f255aa57b91579ae9e98c88704955fa9dac3e5614fb378291155df/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b2cd9e04277e756a2e2d2543d65d1e2166d6fd4c9b183f8808634fda23f17b14", size = 488722 }, + { url = "https://files.pythonhosted.org/packages/49/36/506447b73eb46c120169dc1717fe2eff07c234bb3232a7200b5f5bd816e9/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f3f58818dc0b07f7d9aa7fe9eb1037aecb9700e63e1f6acfed13e9fef648f5d", size = 596088 }, + { url = "https://files.pythonhosted.org/packages/82/ab/5f39e752a9838ec4d52e9b87c1e80f1ee3ccdbe92e183c15b6577ab9de16/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9bb9f66367023ae783551042d31b1d7fd422e8289eedd91f26754a66f44d5cff", size = 472923 }, + { url = "https://files.pythonhosted.org/packages/af/b9/a419292f05e302dea372fa7e6fda5178a92998411f8581b9830d28fb9edb/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aebfd0861a83e6c3d1110b78ad54704486555246e542be3e2bb94195eabb2606", size = 456080 }, + { url = "https://files.pythonhosted.org/packages/b0/c3/d5932fd62bde1a30c36e10c409dc5d54506726f08cb3e1d8d0ba5e2bc8db/watchfiles-1.1.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5fac835b4ab3c6487b5dbad78c4b3724e26bcc468e886f8ba8cc4306f68f6701", size = 629432 }, + { url = "https://files.pythonhosted.org/packages/f7/77/16bddd9779fafb795f1a94319dc965209c5641db5bf1edbbccace6d1b3c0/watchfiles-1.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:399600947b170270e80134ac854e21b3ccdefa11a9529a3decc1327088180f10", size = 623046 }, + { url = "https://files.pythonhosted.org/packages/46/ef/f2ecb9a0f342b4bfad13a2787155c6ee7ce792140eac63a34676a2feeef2/watchfiles-1.1.1-cp311-cp311-win32.whl", hash = "sha256:de6da501c883f58ad50db3a32ad397b09ad29865b5f26f64c24d3e3281685849", size = 271473 }, + { url = "https://files.pythonhosted.org/packages/94/bc/f42d71125f19731ea435c3948cad148d31a64fccde3867e5ba4edee901f9/watchfiles-1.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:35c53bd62a0b885bf653ebf6b700d1bf05debb78ad9292cf2a942b23513dc4c4", size = 287598 }, + { url = "https://files.pythonhosted.org/packages/57/c9/a30f897351f95bbbfb6abcadafbaca711ce1162f4db95fc908c98a9165f3/watchfiles-1.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:57ca5281a8b5e27593cb7d82c2ac927ad88a96ed406aa446f6344e4328208e9e", size = 277210 }, + { url = "https://files.pythonhosted.org/packages/74/d5/f039e7e3c639d9b1d09b07ea412a6806d38123f0508e5f9b48a87b0a76cc/watchfiles-1.1.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:8c89f9f2f740a6b7dcc753140dd5e1ab9215966f7a3530d0c0705c83b401bd7d", size = 404745 }, + { url = "https://files.pythonhosted.org/packages/a5/96/a881a13aa1349827490dab2d363c8039527060cfcc2c92cc6d13d1b1049e/watchfiles-1.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bd404be08018c37350f0d6e34676bd1e2889990117a2b90070b3007f172d0610", size = 391769 }, + { url = "https://files.pythonhosted.org/packages/4b/5b/d3b460364aeb8da471c1989238ea0e56bec24b6042a68046adf3d9ddb01c/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8526e8f916bb5b9a0a777c8317c23ce65de259422bba5b31325a6fa6029d33af", size = 449374 }, + { url = "https://files.pythonhosted.org/packages/b9/44/5769cb62d4ed055cb17417c0a109a92f007114a4e07f30812a73a4efdb11/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2edc3553362b1c38d9f06242416a5d8e9fe235c204a4072e988ce2e5bb1f69f6", size = 459485 }, + { url = "https://files.pythonhosted.org/packages/19/0c/286b6301ded2eccd4ffd0041a1b726afda999926cf720aab63adb68a1e36/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30f7da3fb3f2844259cba4720c3fc7138eb0f7b659c38f3bfa65084c7fc7abce", size = 488813 }, + { url = "https://files.pythonhosted.org/packages/c7/2b/8530ed41112dd4a22f4dcfdb5ccf6a1baad1ff6eed8dc5a5f09e7e8c41c7/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8979280bdafff686ba5e4d8f97840f929a87ed9cdf133cbbd42f7766774d2aa", size = 594816 }, + { url = "https://files.pythonhosted.org/packages/ce/d2/f5f9fb49489f184f18470d4f99f4e862a4b3e9ac2865688eb2099e3d837a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dcc5c24523771db3a294c77d94771abcfcb82a0e0ee8efd910c37c59ec1b31bb", size = 475186 }, + { url = "https://files.pythonhosted.org/packages/cf/68/5707da262a119fb06fbe214d82dd1fe4a6f4af32d2d14de368d0349eb52a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db5d7ae38ff20153d542460752ff397fcf5c96090c1230803713cf3147a6803", size = 456812 }, + { url = "https://files.pythonhosted.org/packages/66/ab/3cbb8756323e8f9b6f9acb9ef4ec26d42b2109bce830cc1f3468df20511d/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:28475ddbde92df1874b6c5c8aaeb24ad5be47a11f87cde5a28ef3835932e3e94", size = 630196 }, + { url = "https://files.pythonhosted.org/packages/78/46/7152ec29b8335f80167928944a94955015a345440f524d2dfe63fc2f437b/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:36193ed342f5b9842edd3532729a2ad55c4160ffcfa3700e0d54be496b70dd43", size = 622657 }, + { url = "https://files.pythonhosted.org/packages/0a/bf/95895e78dd75efe9a7f31733607f384b42eb5feb54bd2eb6ed57cc2e94f4/watchfiles-1.1.1-cp312-cp312-win32.whl", hash = "sha256:859e43a1951717cc8de7f4c77674a6d389b106361585951d9e69572823f311d9", size = 272042 }, + { url = "https://files.pythonhosted.org/packages/87/0a/90eb755f568de2688cb220171c4191df932232c20946966c27a59c400850/watchfiles-1.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:91d4c9a823a8c987cce8fa2690923b069966dabb196dd8d137ea2cede885fde9", size = 288410 }, + { url = "https://files.pythonhosted.org/packages/36/76/f322701530586922fbd6723c4f91ace21364924822a8772c549483abed13/watchfiles-1.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:a625815d4a2bdca61953dbba5a39d60164451ef34c88d751f6c368c3ea73d404", size = 278209 }, + { url = "https://files.pythonhosted.org/packages/bb/f4/f750b29225fe77139f7ae5de89d4949f5a99f934c65a1f1c0b248f26f747/watchfiles-1.1.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:130e4876309e8686a5e37dba7d5e9bc77e6ed908266996ca26572437a5271e18", size = 404321 }, + { url = "https://files.pythonhosted.org/packages/2b/f9/f07a295cde762644aa4c4bb0f88921d2d141af45e735b965fb2e87858328/watchfiles-1.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5f3bde70f157f84ece3765b42b4a52c6ac1a50334903c6eaf765362f6ccca88a", size = 391783 }, + { url = "https://files.pythonhosted.org/packages/bc/11/fc2502457e0bea39a5c958d86d2cb69e407a4d00b85735ca724bfa6e0d1a/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14e0b1fe858430fc0251737ef3824c54027bedb8c37c38114488b8e131cf8219", size = 449279 }, + { url = "https://files.pythonhosted.org/packages/e3/1f/d66bc15ea0b728df3ed96a539c777acfcad0eb78555ad9efcaa1274688f0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f27db948078f3823a6bb3b465180db8ebecf26dd5dae6f6180bd87383b6b4428", size = 459405 }, + { url = "https://files.pythonhosted.org/packages/be/90/9f4a65c0aec3ccf032703e6db02d89a157462fbb2cf20dd415128251cac0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:059098c3a429f62fc98e8ec62b982230ef2c8df68c79e826e37b895bc359a9c0", size = 488976 }, + { url = "https://files.pythonhosted.org/packages/37/57/ee347af605d867f712be7029bb94c8c071732a4b44792e3176fa3c612d39/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfb5862016acc9b869bb57284e6cb35fdf8e22fe59f7548858e2f971d045f150", size = 595506 }, + { url = "https://files.pythonhosted.org/packages/a8/78/cc5ab0b86c122047f75e8fc471c67a04dee395daf847d3e59381996c8707/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:319b27255aacd9923b8a276bb14d21a5f7ff82564c744235fc5eae58d95422ae", size = 474936 }, + { url = "https://files.pythonhosted.org/packages/62/da/def65b170a3815af7bd40a3e7010bf6ab53089ef1b75d05dd5385b87cf08/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c755367e51db90e75b19454b680903631d41f9e3607fbd941d296a020c2d752d", size = 456147 }, + { url = "https://files.pythonhosted.org/packages/57/99/da6573ba71166e82d288d4df0839128004c67d2778d3b566c138695f5c0b/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c22c776292a23bfc7237a98f791b9ad3144b02116ff10d820829ce62dff46d0b", size = 630007 }, + { url = "https://files.pythonhosted.org/packages/a8/51/7439c4dd39511368849eb1e53279cd3454b4a4dbace80bab88feeb83c6b5/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:3a476189be23c3686bc2f4321dd501cb329c0a0469e77b7b534ee10129ae6374", size = 622280 }, + { url = "https://files.pythonhosted.org/packages/95/9c/8ed97d4bba5db6fdcdb2b298d3898f2dd5c20f6b73aee04eabe56c59677e/watchfiles-1.1.1-cp313-cp313-win32.whl", hash = "sha256:bf0a91bfb5574a2f7fc223cf95eeea79abfefa404bf1ea5e339c0c1560ae99a0", size = 272056 }, + { url = "https://files.pythonhosted.org/packages/1f/f3/c14e28429f744a260d8ceae18bf58c1d5fa56b50d006a7a9f80e1882cb0d/watchfiles-1.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:52e06553899e11e8074503c8e716d574adeeb7e68913115c4b3653c53f9bae42", size = 288162 }, + { url = "https://files.pythonhosted.org/packages/dc/61/fe0e56c40d5cd29523e398d31153218718c5786b5e636d9ae8ae79453d27/watchfiles-1.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:ac3cc5759570cd02662b15fbcd9d917f7ecd47efe0d6b40474eafd246f91ea18", size = 277909 }, + { url = "https://files.pythonhosted.org/packages/79/42/e0a7d749626f1e28c7108a99fb9bf524b501bbbeb9b261ceecde644d5a07/watchfiles-1.1.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:563b116874a9a7ce6f96f87cd0b94f7faf92d08d0021e837796f0a14318ef8da", size = 403389 }, + { url = "https://files.pythonhosted.org/packages/15/49/08732f90ce0fbbc13913f9f215c689cfc9ced345fb1bcd8829a50007cc8d/watchfiles-1.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3ad9fe1dae4ab4212d8c91e80b832425e24f421703b5a42ef2e4a1e215aff051", size = 389964 }, + { url = "https://files.pythonhosted.org/packages/27/0d/7c315d4bd5f2538910491a0393c56bf70d333d51bc5b34bee8e68e8cea19/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce70f96a46b894b36eba678f153f052967a0d06d5b5a19b336ab0dbbd029f73e", size = 448114 }, + { url = "https://files.pythonhosted.org/packages/c3/24/9e096de47a4d11bc4df41e9d1e61776393eac4cb6eb11b3e23315b78b2cc/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cb467c999c2eff23a6417e58d75e5828716f42ed8289fe6b77a7e5a91036ca70", size = 460264 }, + { url = "https://files.pythonhosted.org/packages/cc/0f/e8dea6375f1d3ba5fcb0b3583e2b493e77379834c74fd5a22d66d85d6540/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:836398932192dae4146c8f6f737d74baeac8b70ce14831a239bdb1ca882fc261", size = 487877 }, + { url = "https://files.pythonhosted.org/packages/ac/5b/df24cfc6424a12deb41503b64d42fbea6b8cb357ec62ca84a5a3476f654a/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:743185e7372b7bc7c389e1badcc606931a827112fbbd37f14c537320fca08620", size = 595176 }, + { url = "https://files.pythonhosted.org/packages/8f/b5/853b6757f7347de4e9b37e8cc3289283fb983cba1ab4d2d7144694871d9c/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:afaeff7696e0ad9f02cbb8f56365ff4686ab205fcf9c4c5b6fdfaaa16549dd04", size = 473577 }, + { url = "https://files.pythonhosted.org/packages/e1/f7/0a4467be0a56e80447c8529c9fce5b38eab4f513cb3d9bf82e7392a5696b/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7eb7da0eb23aa2ba036d4f616d46906013a68caf61b7fdbe42fc8b25132e77", size = 455425 }, + { url = "https://files.pythonhosted.org/packages/8e/e0/82583485ea00137ddf69bc84a2db88bd92ab4a6e3c405e5fb878ead8d0e7/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:831a62658609f0e5c64178211c942ace999517f5770fe9436be4c2faeba0c0ef", size = 628826 }, + { url = "https://files.pythonhosted.org/packages/28/9a/a785356fccf9fae84c0cc90570f11702ae9571036fb25932f1242c82191c/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:f9a2ae5c91cecc9edd47e041a930490c31c3afb1f5e6d71de3dc671bfaca02bf", size = 622208 }, + { url = "https://files.pythonhosted.org/packages/c3/f4/0872229324ef69b2c3edec35e84bd57a1289e7d3fe74588048ed8947a323/watchfiles-1.1.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:d1715143123baeeaeadec0528bb7441103979a1d5f6fd0e1f915383fea7ea6d5", size = 404315 }, + { url = "https://files.pythonhosted.org/packages/7b/22/16d5331eaed1cb107b873f6ae1b69e9ced582fcf0c59a50cd84f403b1c32/watchfiles-1.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:39574d6370c4579d7f5d0ad940ce5b20db0e4117444e39b6d8f99db5676c52fd", size = 390869 }, + { url = "https://files.pythonhosted.org/packages/b2/7e/5643bfff5acb6539b18483128fdc0ef2cccc94a5b8fbda130c823e8ed636/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7365b92c2e69ee952902e8f70f3ba6360d0d596d9299d55d7d386df84b6941fb", size = 449919 }, + { url = "https://files.pythonhosted.org/packages/51/2e/c410993ba5025a9f9357c376f48976ef0e1b1aefb73b97a5ae01a5972755/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bfff9740c69c0e4ed32416f013f3c45e2ae42ccedd1167ef2d805c000b6c71a5", size = 460845 }, + { url = "https://files.pythonhosted.org/packages/8e/a4/2df3b404469122e8680f0fcd06079317e48db58a2da2950fb45020947734/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b27cf2eb1dda37b2089e3907d8ea92922b673c0c427886d4edc6b94d8dfe5db3", size = 489027 }, + { url = "https://files.pythonhosted.org/packages/ea/84/4587ba5b1f267167ee715b7f66e6382cca6938e0a4b870adad93e44747e6/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:526e86aced14a65a5b0ec50827c745597c782ff46b571dbfe46192ab9e0b3c33", size = 595615 }, + { url = "https://files.pythonhosted.org/packages/6a/0f/c6988c91d06e93cd0bb3d4a808bcf32375ca1904609835c3031799e3ecae/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04e78dd0b6352db95507fd8cb46f39d185cf8c74e4cf1e4fbad1d3df96faf510", size = 474836 }, + { url = "https://files.pythonhosted.org/packages/b4/36/ded8aebea91919485b7bbabbd14f5f359326cb5ec218cd67074d1e426d74/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c85794a4cfa094714fb9c08d4a218375b2b95b8ed1666e8677c349906246c05", size = 455099 }, + { url = "https://files.pythonhosted.org/packages/98/e0/8c9bdba88af756a2fce230dd365fab2baf927ba42cd47521ee7498fd5211/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:74d5012b7630714b66be7b7b7a78855ef7ad58e8650c73afc4c076a1f480a8d6", size = 630626 }, + { url = "https://files.pythonhosted.org/packages/2a/84/a95db05354bf2d19e438520d92a8ca475e578c647f78f53197f5a2f17aaf/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:8fbe85cb3201c7d380d3d0b90e63d520f15d6afe217165d7f98c9c649654db81", size = 622519 }, + { url = "https://files.pythonhosted.org/packages/1d/ce/d8acdc8de545de995c339be67711e474c77d643555a9bb74a9334252bd55/watchfiles-1.1.1-cp314-cp314-win32.whl", hash = "sha256:3fa0b59c92278b5a7800d3ee7733da9d096d4aabcfabb9a928918bd276ef9b9b", size = 272078 }, + { url = "https://files.pythonhosted.org/packages/c4/c9/a74487f72d0451524be827e8edec251da0cc1fcf111646a511ae752e1a3d/watchfiles-1.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:c2047d0b6cea13b3316bdbafbfa0c4228ae593d995030fda39089d36e64fc03a", size = 287664 }, + { url = "https://files.pythonhosted.org/packages/df/b8/8ac000702cdd496cdce998c6f4ee0ca1f15977bba51bdf07d872ebdfc34c/watchfiles-1.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:842178b126593addc05acf6fce960d28bc5fae7afbaa2c6c1b3a7b9460e5be02", size = 277154 }, + { url = "https://files.pythonhosted.org/packages/47/a8/e3af2184707c29f0f14b1963c0aace6529f9d1b8582d5b99f31bbf42f59e/watchfiles-1.1.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:88863fbbc1a7312972f1c511f202eb30866370ebb8493aef2812b9ff28156a21", size = 403820 }, + { url = "https://files.pythonhosted.org/packages/c0/ec/e47e307c2f4bd75f9f9e8afbe3876679b18e1bcec449beca132a1c5ffb2d/watchfiles-1.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:55c7475190662e202c08c6c0f4d9e345a29367438cf8e8037f3155e10a88d5a5", size = 390510 }, + { url = "https://files.pythonhosted.org/packages/d5/a0/ad235642118090f66e7b2f18fd5c42082418404a79205cdfca50b6309c13/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f53fa183d53a1d7a8852277c92b967ae99c2d4dcee2bfacff8868e6e30b15f7", size = 448408 }, + { url = "https://files.pythonhosted.org/packages/df/85/97fa10fd5ff3332ae17e7e40e20784e419e28521549780869f1413742e9d/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6aae418a8b323732fa89721d86f39ec8f092fc2af67f4217a2b07fd3e93c6101", size = 458968 }, + { url = "https://files.pythonhosted.org/packages/47/c2/9059c2e8966ea5ce678166617a7f75ecba6164375f3b288e50a40dc6d489/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f096076119da54a6080e8920cbdaac3dbee667eb91dcc5e5b78840b87415bd44", size = 488096 }, + { url = "https://files.pythonhosted.org/packages/94/44/d90a9ec8ac309bc26db808a13e7bfc0e4e78b6fc051078a554e132e80160/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00485f441d183717038ed2e887a7c868154f216877653121068107b227a2f64c", size = 596040 }, + { url = "https://files.pythonhosted.org/packages/95/68/4e3479b20ca305cfc561db3ed207a8a1c745ee32bf24f2026a129d0ddb6e/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a55f3e9e493158d7bfdb60a1165035f1cf7d320914e7b7ea83fe22c6023b58fc", size = 473847 }, + { url = "https://files.pythonhosted.org/packages/4f/55/2af26693fd15165c4ff7857e38330e1b61ab8c37d15dc79118cdba115b7a/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c91ed27800188c2ae96d16e3149f199d62f86c7af5f5f4d2c61a3ed8cd3666c", size = 455072 }, + { url = "https://files.pythonhosted.org/packages/66/1d/d0d200b10c9311ec25d2273f8aad8c3ef7cc7ea11808022501811208a750/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:311ff15a0bae3714ffb603e6ba6dbfba4065ab60865d15a6ec544133bdb21099", size = 629104 }, + { url = "https://files.pythonhosted.org/packages/e3/bd/fa9bb053192491b3867ba07d2343d9f2252e00811567d30ae8d0f78136fe/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a916a2932da8f8ab582f242c065f5c81bed3462849ca79ee357dd9551b0e9b01", size = 622112 }, + { url = "https://files.pythonhosted.org/packages/ba/4c/a888c91e2e326872fa4705095d64acd8aa2fb9c1f7b9bd0588f33850516c/watchfiles-1.1.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:17ef139237dfced9da49fb7f2232c86ca9421f666d78c264c7ffca6601d154c3", size = 409611 }, + { url = "https://files.pythonhosted.org/packages/1e/c7/5420d1943c8e3ce1a21c0a9330bcf7edafb6aa65d26b21dbb3267c9e8112/watchfiles-1.1.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:672b8adf25b1a0d35c96b5888b7b18699d27d4194bac8beeae75be4b7a3fc9b2", size = 396889 }, + { url = "https://files.pythonhosted.org/packages/0c/e5/0072cef3804ce8d3aaddbfe7788aadff6b3d3f98a286fdbee9fd74ca59a7/watchfiles-1.1.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77a13aea58bc2b90173bc69f2a90de8e282648939a00a602e1dc4ee23e26b66d", size = 451616 }, + { url = "https://files.pythonhosted.org/packages/83/4e/b87b71cbdfad81ad7e83358b3e447fedd281b880a03d64a760fe0a11fc2e/watchfiles-1.1.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b495de0bb386df6a12b18335a0285dda90260f51bdb505503c02bcd1ce27a8b", size = 458413 }, + { url = "https://files.pythonhosted.org/packages/d3/8e/e500f8b0b77be4ff753ac94dc06b33d8f0d839377fee1b78e8c8d8f031bf/watchfiles-1.1.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:db476ab59b6765134de1d4fe96a1a9c96ddf091683599be0f26147ea1b2e4b88", size = 408250 }, + { url = "https://files.pythonhosted.org/packages/bd/95/615e72cd27b85b61eec764a5ca51bd94d40b5adea5ff47567d9ebc4d275a/watchfiles-1.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:89eef07eee5e9d1fda06e38822ad167a044153457e6fd997f8a858ab7564a336", size = 396117 }, + { url = "https://files.pythonhosted.org/packages/c9/81/e7fe958ce8a7fb5c73cc9fb07f5aeaf755e6aa72498c57d760af760c91f8/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce19e06cbda693e9e7686358af9cd6f5d61312ab8b00488bc36f5aabbaf77e24", size = 450493 }, + { url = "https://files.pythonhosted.org/packages/6e/d4/ed38dd3b1767193de971e694aa544356e63353c33a85d948166b5ff58b9e/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e6f39af2eab0118338902798b5aa6664f46ff66bc0280de76fca67a7f262a49", size = 457546 }, +] + +[[package]] +name = "websockets" +version = "16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/04/24/4b2031d72e840ce4c1ccb255f693b15c334757fc50023e4db9537080b8c4/websockets-16.0.tar.gz", hash = "sha256:5f6261a5e56e8d5c42a4497b364ea24d94d9563e8fbd44e78ac40879c60179b5", size = 179346 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/74/221f58decd852f4b59cc3354cccaf87e8ef695fede361d03dc9a7396573b/websockets-16.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:04cdd5d2d1dacbad0a7bf36ccbcd3ccd5a30ee188f2560b7a62a30d14107b31a", size = 177343 }, + { url = "https://files.pythonhosted.org/packages/19/0f/22ef6107ee52ab7f0b710d55d36f5a5d3ef19e8a205541a6d7ffa7994e5a/websockets-16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8ff32bb86522a9e5e31439a58addbb0166f0204d64066fb955265c4e214160f0", size = 175021 }, + { url = "https://files.pythonhosted.org/packages/10/40/904a4cb30d9b61c0e278899bf36342e9b0208eb3c470324a9ecbaac2a30f/websockets-16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:583b7c42688636f930688d712885cf1531326ee05effd982028212ccc13e5957", size = 175320 }, + { url = "https://files.pythonhosted.org/packages/9d/2f/4b3ca7e106bc608744b1cdae041e005e446124bebb037b18799c2d356864/websockets-16.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7d837379b647c0c4c2355c2499723f82f1635fd2c26510e1f587d89bc2199e72", size = 183815 }, + { url = "https://files.pythonhosted.org/packages/86/26/d40eaa2a46d4302becec8d15b0fc5e45bdde05191e7628405a19cf491ccd/websockets-16.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df57afc692e517a85e65b72e165356ed1df12386ecb879ad5693be08fac65dde", size = 185054 }, + { url = "https://files.pythonhosted.org/packages/b0/ba/6500a0efc94f7373ee8fefa8c271acdfd4dca8bd49a90d4be7ccabfc397e/websockets-16.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2b9f1e0d69bc60a4a87349d50c09a037a2607918746f07de04df9e43252c77a3", size = 184565 }, + { url = "https://files.pythonhosted.org/packages/04/b4/96bf2cee7c8d8102389374a2616200574f5f01128d1082f44102140344cc/websockets-16.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:335c23addf3d5e6a8633f9f8eda77efad001671e80b95c491dd0924587ece0b3", size = 183848 }, + { url = "https://files.pythonhosted.org/packages/02/8e/81f40fb00fd125357814e8c3025738fc4ffc3da4b6b4a4472a82ba304b41/websockets-16.0-cp310-cp310-win32.whl", hash = "sha256:37b31c1623c6605e4c00d466c9d633f9b812ea430c11c8a278774a1fde1acfa9", size = 178249 }, + { url = "https://files.pythonhosted.org/packages/b4/5f/7e40efe8df57db9b91c88a43690ac66f7b7aa73a11aa6a66b927e44f26fa/websockets-16.0-cp310-cp310-win_amd64.whl", hash = "sha256:8e1dab317b6e77424356e11e99a432b7cb2f3ec8c5ab4dabbcee6add48f72b35", size = 178685 }, + { url = "https://files.pythonhosted.org/packages/f2/db/de907251b4ff46ae804ad0409809504153b3f30984daf82a1d84a9875830/websockets-16.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:31a52addea25187bde0797a97d6fc3d2f92b6f72a9370792d65a6e84615ac8a8", size = 177340 }, + { url = "https://files.pythonhosted.org/packages/f3/fa/abe89019d8d8815c8781e90d697dec52523fb8ebe308bf11664e8de1877e/websockets-16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:417b28978cdccab24f46400586d128366313e8a96312e4b9362a4af504f3bbad", size = 175022 }, + { url = "https://files.pythonhosted.org/packages/58/5d/88ea17ed1ded2079358b40d31d48abe90a73c9e5819dbcde1606e991e2ad/websockets-16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:af80d74d4edfa3cb9ed973a0a5ba2b2a549371f8a741e0800cb07becdd20f23d", size = 175319 }, + { url = "https://files.pythonhosted.org/packages/d2/ae/0ee92b33087a33632f37a635e11e1d99d429d3d323329675a6022312aac2/websockets-16.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:08d7af67b64d29823fed316505a89b86705f2b7981c07848fb5e3ea3020c1abe", size = 184631 }, + { url = "https://files.pythonhosted.org/packages/c8/c5/27178df583b6c5b31b29f526ba2da5e2f864ecc79c99dae630a85d68c304/websockets-16.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7be95cfb0a4dae143eaed2bcba8ac23f4892d8971311f1b06f3c6b78952ee70b", size = 185870 }, + { url = "https://files.pythonhosted.org/packages/87/05/536652aa84ddc1c018dbb7e2c4cbcd0db884580bf8e95aece7593fde526f/websockets-16.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d6297ce39ce5c2e6feb13c1a996a2ded3b6832155fcfc920265c76f24c7cceb5", size = 185361 }, + { url = "https://files.pythonhosted.org/packages/6d/e2/d5332c90da12b1e01f06fb1b85c50cfc489783076547415bf9f0a659ec19/websockets-16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1c1b30e4f497b0b354057f3467f56244c603a79c0d1dafce1d16c283c25f6e64", size = 184615 }, + { url = "https://files.pythonhosted.org/packages/77/fb/d3f9576691cae9253b51555f841bc6600bf0a983a461c79500ace5a5b364/websockets-16.0-cp311-cp311-win32.whl", hash = "sha256:5f451484aeb5cafee1ccf789b1b66f535409d038c56966d6101740c1614b86c6", size = 178246 }, + { url = "https://files.pythonhosted.org/packages/54/67/eaff76b3dbaf18dcddabc3b8c1dba50b483761cccff67793897945b37408/websockets-16.0-cp311-cp311-win_amd64.whl", hash = "sha256:8d7f0659570eefb578dacde98e24fb60af35350193e4f56e11190787bee77dac", size = 178684 }, + { url = "https://files.pythonhosted.org/packages/84/7b/bac442e6b96c9d25092695578dda82403c77936104b5682307bd4deb1ad4/websockets-16.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:71c989cbf3254fbd5e84d3bff31e4da39c43f884e64f2551d14bb3c186230f00", size = 177365 }, + { url = "https://files.pythonhosted.org/packages/b0/fe/136ccece61bd690d9c1f715baaeefd953bb2360134de73519d5df19d29ca/websockets-16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8b6e209ffee39ff1b6d0fa7bfef6de950c60dfb91b8fcead17da4ee539121a79", size = 175038 }, + { url = "https://files.pythonhosted.org/packages/40/1e/9771421ac2286eaab95b8575b0cb701ae3663abf8b5e1f64f1fd90d0a673/websockets-16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:86890e837d61574c92a97496d590968b23c2ef0aeb8a9bc9421d174cd378ae39", size = 175328 }, + { url = "https://files.pythonhosted.org/packages/18/29/71729b4671f21e1eaa5d6573031ab810ad2936c8175f03f97f3ff164c802/websockets-16.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9b5aca38b67492ef518a8ab76851862488a478602229112c4b0d58d63a7a4d5c", size = 184915 }, + { url = "https://files.pythonhosted.org/packages/97/bb/21c36b7dbbafc85d2d480cd65df02a1dc93bf76d97147605a8e27ff9409d/websockets-16.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e0334872c0a37b606418ac52f6ab9cfd17317ac26365f7f65e203e2d0d0d359f", size = 186152 }, + { url = "https://files.pythonhosted.org/packages/4a/34/9bf8df0c0cf88fa7bfe36678dc7b02970c9a7d5e065a3099292db87b1be2/websockets-16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a0b31e0b424cc6b5a04b8838bbaec1688834b2383256688cf47eb97412531da1", size = 185583 }, + { url = "https://files.pythonhosted.org/packages/47/88/4dd516068e1a3d6ab3c7c183288404cd424a9a02d585efbac226cb61ff2d/websockets-16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:485c49116d0af10ac698623c513c1cc01c9446c058a4e61e3bf6c19dff7335a2", size = 184880 }, + { url = "https://files.pythonhosted.org/packages/91/d6/7d4553ad4bf1c0421e1ebd4b18de5d9098383b5caa1d937b63df8d04b565/websockets-16.0-cp312-cp312-win32.whl", hash = "sha256:eaded469f5e5b7294e2bdca0ab06becb6756ea86894a47806456089298813c89", size = 178261 }, + { url = "https://files.pythonhosted.org/packages/c3/f0/f3a17365441ed1c27f850a80b2bc680a0fa9505d733fe152fdf5e98c1c0b/websockets-16.0-cp312-cp312-win_amd64.whl", hash = "sha256:5569417dc80977fc8c2d43a86f78e0a5a22fee17565d78621b6bb264a115d4ea", size = 178693 }, + { url = "https://files.pythonhosted.org/packages/cc/9c/baa8456050d1c1b08dd0ec7346026668cbc6f145ab4e314d707bb845bf0d/websockets-16.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:878b336ac47938b474c8f982ac2f7266a540adc3fa4ad74ae96fea9823a02cc9", size = 177364 }, + { url = "https://files.pythonhosted.org/packages/7e/0c/8811fc53e9bcff68fe7de2bcbe75116a8d959ac699a3200f4847a8925210/websockets-16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:52a0fec0e6c8d9a784c2c78276a48a2bdf099e4ccc2a4cad53b27718dbfd0230", size = 175039 }, + { url = "https://files.pythonhosted.org/packages/aa/82/39a5f910cb99ec0b59e482971238c845af9220d3ab9fa76dd9162cda9d62/websockets-16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e6578ed5b6981005df1860a56e3617f14a6c307e6a71b4fff8c48fdc50f3ed2c", size = 175323 }, + { url = "https://files.pythonhosted.org/packages/bd/28/0a25ee5342eb5d5f297d992a77e56892ecb65e7854c7898fb7d35e9b33bd/websockets-16.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:95724e638f0f9c350bb1c2b0a7ad0e83d9cc0c9259f3ea94e40d7b02a2179ae5", size = 184975 }, + { url = "https://files.pythonhosted.org/packages/f9/66/27ea52741752f5107c2e41fda05e8395a682a1e11c4e592a809a90c6a506/websockets-16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0204dc62a89dc9d50d682412c10b3542d748260d743500a85c13cd1ee4bde82", size = 186203 }, + { url = "https://files.pythonhosted.org/packages/37/e5/8e32857371406a757816a2b471939d51c463509be73fa538216ea52b792a/websockets-16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:52ac480f44d32970d66763115edea932f1c5b1312de36df06d6b219f6741eed8", size = 185653 }, + { url = "https://files.pythonhosted.org/packages/9b/67/f926bac29882894669368dc73f4da900fcdf47955d0a0185d60103df5737/websockets-16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6e5a82b677f8f6f59e8dfc34ec06ca6b5b48bc4fcda346acd093694cc2c24d8f", size = 184920 }, + { url = "https://files.pythonhosted.org/packages/3c/a1/3d6ccdcd125b0a42a311bcd15a7f705d688f73b2a22d8cf1c0875d35d34a/websockets-16.0-cp313-cp313-win32.whl", hash = "sha256:abf050a199613f64c886ea10f38b47770a65154dc37181bfaff70c160f45315a", size = 178255 }, + { url = "https://files.pythonhosted.org/packages/6b/ae/90366304d7c2ce80f9b826096a9e9048b4bb760e44d3b873bb272cba696b/websockets-16.0-cp313-cp313-win_amd64.whl", hash = "sha256:3425ac5cf448801335d6fdc7ae1eb22072055417a96cc6b31b3861f455fbc156", size = 178689 }, + { url = "https://files.pythonhosted.org/packages/f3/1d/e88022630271f5bd349ed82417136281931e558d628dd52c4d8621b4a0b2/websockets-16.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8cc451a50f2aee53042ac52d2d053d08bf89bcb31ae799cb4487587661c038a0", size = 177406 }, + { url = "https://files.pythonhosted.org/packages/f2/78/e63be1bf0724eeb4616efb1ae1c9044f7c3953b7957799abb5915bffd38e/websockets-16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:daa3b6ff70a9241cf6c7fc9e949d41232d9d7d26fd3522b1ad2b4d62487e9904", size = 175085 }, + { url = "https://files.pythonhosted.org/packages/bb/f4/d3c9220d818ee955ae390cf319a7c7a467beceb24f05ee7aaaa2414345ba/websockets-16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fd3cb4adb94a2a6e2b7c0d8d05cb94e6f1c81a0cf9dc2694fb65c7e8d94c42e4", size = 175328 }, + { url = "https://files.pythonhosted.org/packages/63/bc/d3e208028de777087e6fb2b122051a6ff7bbcca0d6df9d9c2bf1dd869ae9/websockets-16.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:781caf5e8eee67f663126490c2f96f40906594cb86b408a703630f95550a8c3e", size = 185044 }, + { url = "https://files.pythonhosted.org/packages/ad/6e/9a0927ac24bd33a0a9af834d89e0abc7cfd8e13bed17a86407a66773cc0e/websockets-16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:caab51a72c51973ca21fa8a18bd8165e1a0183f1ac7066a182ff27107b71e1a4", size = 186279 }, + { url = "https://files.pythonhosted.org/packages/b9/ca/bf1c68440d7a868180e11be653c85959502efd3a709323230314fda6e0b3/websockets-16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19c4dc84098e523fd63711e563077d39e90ec6702aff4b5d9e344a60cb3c0cb1", size = 185711 }, + { url = "https://files.pythonhosted.org/packages/c4/f8/fdc34643a989561f217bb477cbc47a3a07212cbda91c0e4389c43c296ebf/websockets-16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a5e18a238a2b2249c9a9235466b90e96ae4795672598a58772dd806edc7ac6d3", size = 184982 }, + { url = "https://files.pythonhosted.org/packages/dd/d1/574fa27e233764dbac9c52730d63fcf2823b16f0856b3329fc6268d6ae4f/websockets-16.0-cp314-cp314-win32.whl", hash = "sha256:a069d734c4a043182729edd3e9f247c3b2a4035415a9172fd0f1b71658a320a8", size = 177915 }, + { url = "https://files.pythonhosted.org/packages/8a/f1/ae6b937bf3126b5134ce1f482365fde31a357c784ac51852978768b5eff4/websockets-16.0-cp314-cp314-win_amd64.whl", hash = "sha256:c0ee0e63f23914732c6d7e0cce24915c48f3f1512ec1d079ed01fc629dab269d", size = 178381 }, + { url = "https://files.pythonhosted.org/packages/06/9b/f791d1db48403e1f0a27577a6beb37afae94254a8c6f08be4a23e4930bc0/websockets-16.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:a35539cacc3febb22b8f4d4a99cc79b104226a756aa7400adc722e83b0d03244", size = 177737 }, + { url = "https://files.pythonhosted.org/packages/bd/40/53ad02341fa33b3ce489023f635367a4ac98b73570102ad2cdd770dacc9a/websockets-16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b784ca5de850f4ce93ec85d3269d24d4c82f22b7212023c974c401d4980ebc5e", size = 175268 }, + { url = "https://files.pythonhosted.org/packages/74/9b/6158d4e459b984f949dcbbb0c5d270154c7618e11c01029b9bbd1bb4c4f9/websockets-16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:569d01a4e7fba956c5ae4fc988f0d4e187900f5497ce46339c996dbf24f17641", size = 175486 }, + { url = "https://files.pythonhosted.org/packages/e5/2d/7583b30208b639c8090206f95073646c2c9ffd66f44df967981a64f849ad/websockets-16.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:50f23cdd8343b984957e4077839841146f67a3d31ab0d00e6b824e74c5b2f6e8", size = 185331 }, + { url = "https://files.pythonhosted.org/packages/45/b0/cce3784eb519b7b5ad680d14b9673a31ab8dcb7aad8b64d81709d2430aa8/websockets-16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:152284a83a00c59b759697b7f9e9cddf4e3c7861dd0d964b472b70f78f89e80e", size = 186501 }, + { url = "https://files.pythonhosted.org/packages/19/60/b8ebe4c7e89fb5f6cdf080623c9d92789a53636950f7abacfc33fe2b3135/websockets-16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bc59589ab64b0022385f429b94697348a6a234e8ce22544e3681b2e9331b5944", size = 186062 }, + { url = "https://files.pythonhosted.org/packages/88/a8/a080593f89b0138b6cba1b28f8df5673b5506f72879322288b031337c0b8/websockets-16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32da954ffa2814258030e5a57bc73a3635463238e797c7375dc8091327434206", size = 185356 }, + { url = "https://files.pythonhosted.org/packages/c2/b6/b9afed2afadddaf5ebb2afa801abf4b0868f42f8539bfe4b071b5266c9fe/websockets-16.0-cp314-cp314t-win32.whl", hash = "sha256:5a4b4cc550cb665dd8a47f868c8d04c8230f857363ad3c9caf7a0c3bf8c61ca6", size = 178085 }, + { url = "https://files.pythonhosted.org/packages/9f/3e/28135a24e384493fa804216b79a6a6759a38cc4ff59118787b9fb693df93/websockets-16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b14dc141ed6d2dde437cddb216004bcac6a1df0935d79656387bd41632ba0bbd", size = 178531 }, + { url = "https://files.pythonhosted.org/packages/72/07/c98a68571dcf256e74f1f816b8cc5eae6eb2d3d5cfa44d37f801619d9166/websockets-16.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:349f83cd6c9a415428ee1005cadb5c2c56f4389bc06a9af16103c3bc3dcc8b7d", size = 174947 }, + { url = "https://files.pythonhosted.org/packages/7e/52/93e166a81e0305b33fe416338be92ae863563fe7bce446b0f687b9df5aea/websockets-16.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:4a1aba3340a8dca8db6eb5a7986157f52eb9e436b74813764241981ca4888f03", size = 175260 }, + { url = "https://files.pythonhosted.org/packages/56/0c/2dbf513bafd24889d33de2ff0368190a0e69f37bcfa19009ef819fe4d507/websockets-16.0-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f4a32d1bd841d4bcbffdcb3d2ce50c09c3909fbead375ab28d0181af89fd04da", size = 176071 }, + { url = "https://files.pythonhosted.org/packages/a5/8f/aea9c71cc92bf9b6cc0f7f70df8f0b420636b6c96ef4feee1e16f80f75dd/websockets-16.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0298d07ee155e2e9fda5be8a9042200dd2e3bb0b8a38482156576f863a9d457c", size = 176968 }, + { url = "https://files.pythonhosted.org/packages/9a/3f/f70e03f40ffc9a30d817eef7da1be72ee4956ba8d7255c399a01b135902a/websockets-16.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:a653aea902e0324b52f1613332ddf50b00c06fdaf7e92624fbf8c77c78fa5767", size = 178735 }, + { url = "https://files.pythonhosted.org/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598 }, +] + +[[package]] +name = "yarl" +version = "1.23.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "multidict" }, + { name = "propcache" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/23/6e/beb1beec874a72f23815c1434518bfc4ed2175065173fb138c3705f658d4/yarl-1.23.0.tar.gz", hash = "sha256:53b1ea6ca88ebd4420379c330aea57e258408dd0df9af0992e5de2078dc9f5d5", size = 194676 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/0d/9cc638702f6fc3c7a3685bcc8cf2a9ed7d6206e932a49f5242658047ef51/yarl-1.23.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cff6d44cb13d39db2663a22b22305d10855efa0fa8015ddeacc40bc59b9d8107", size = 123764 }, + { url = "https://files.pythonhosted.org/packages/7a/35/5a553687c5793df5429cd1db45909d4f3af7eee90014888c208d086a44f0/yarl-1.23.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e4c53f8347cd4200f0d70a48ad059cabaf24f5adc6ba08622a23423bc7efa10d", size = 86282 }, + { url = "https://files.pythonhosted.org/packages/68/2e/c5a2234238f8ce37a8312b52801ee74117f576b1539eec8404a480434acc/yarl-1.23.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2a6940a074fb3c48356ed0158a3ca5699c955ee4185b4d7d619be3c327143e05", size = 86053 }, + { url = "https://files.pythonhosted.org/packages/74/3f/bbd8ff36fb038622797ffbaf7db314918bb4d76f1cc8a4f9ca7a55fe5195/yarl-1.23.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ed5f69ce7be7902e5c70ea19eb72d20abf7d725ab5d49777d696e32d4fc1811d", size = 99395 }, + { url = "https://files.pythonhosted.org/packages/77/04/9516bc4e269d2a3ec9c6779fcdeac51ce5b3a9b0156f06ac7152e5bba864/yarl-1.23.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:389871e65468400d6283c0308e791a640b5ab5c83bcee02a2f51295f95e09748", size = 92143 }, + { url = "https://files.pythonhosted.org/packages/c7/63/88802d1f6b1cb1fc67d67a58cd0cf8a1790de4ce7946e434240f1d60ab4a/yarl-1.23.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dda608c88cf709b1d406bdfcd84d8d63cff7c9e577a403c6108ce8ce9dcc8764", size = 107643 }, + { url = "https://files.pythonhosted.org/packages/8e/db/4f9b838f4d8bdd6f0f385aed8bbf21c71ed11a0b9983305c302cbd557815/yarl-1.23.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8c4fe09e0780c6c3bf2b7d4af02ee2394439d11a523bbcf095cf4747c2932007", size = 108700 }, + { url = "https://files.pythonhosted.org/packages/50/12/95a1d33f04a79c402664070d43b8b9f72dc18914e135b345b611b0b1f8cc/yarl-1.23.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:31c9921eb8bd12633b41ad27686bbb0b1a2a9b8452bfdf221e34f311e9942ed4", size = 102769 }, + { url = "https://files.pythonhosted.org/packages/86/65/91a0285f51321369fd1a8308aa19207520c5f0587772cfc2e03fc2467e90/yarl-1.23.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5f10fd85e4b75967468af655228fbfd212bdf66db1c0d135065ce288982eda26", size = 101114 }, + { url = "https://files.pythonhosted.org/packages/58/80/c7c8244fc3e5bc483dc71a09560f43b619fab29301a0f0a8f936e42865c7/yarl-1.23.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dbf507e9ef5688bada447a24d68b4b58dd389ba93b7afc065a2ba892bea54769", size = 98883 }, + { url = "https://files.pythonhosted.org/packages/86/e7/71ca9cc9ca79c0b7d491216177d1aed559d632947b8ffb0ee60f7d8b23e3/yarl-1.23.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:85e9beda1f591bc73e77ea1c51965c68e98dafd0fec72cdd745f77d727466716", size = 94172 }, + { url = "https://files.pythonhosted.org/packages/6a/3f/6c6c8a0fe29c26fb2db2e8d32195bb84ec1bfb8f1d32e7f73b787fcf349b/yarl-1.23.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:0e1fdaa14ef51366d7757b45bde294e95f6c8c049194e793eedb8387c86d5993", size = 107010 }, + { url = "https://files.pythonhosted.org/packages/56/38/12730c05e5ad40a76374d440ed8b0899729a96c250516d91c620a6e38fc2/yarl-1.23.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:75e3026ab649bf48f9a10c0134512638725b521340293f202a69b567518d94e0", size = 100285 }, + { url = "https://files.pythonhosted.org/packages/34/92/6a7be9239f2347234e027284e7a5f74b1140cc86575e7b469d13fba1ebfe/yarl-1.23.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:80e6d33a3d42a7549b409f199857b4fb54e2103fc44fb87605b6663b7a7ff750", size = 108230 }, + { url = "https://files.pythonhosted.org/packages/5e/81/4aebccfa9376bd98b9d8bfad20621a57d3e8cfc5b8631c1fa5f62cdd03f4/yarl-1.23.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5ec2f42d41ccbd5df0270d7df31618a8ee267bfa50997f5d720ddba86c4a83a6", size = 103008 }, + { url = "https://files.pythonhosted.org/packages/38/0f/0b4e3edcec794a86b853b0c6396c0a888d72dfce19b2d88c02ac289fb6c1/yarl-1.23.0-cp310-cp310-win32.whl", hash = "sha256:debe9c4f41c32990771be5c22b56f810659f9ddf3d63f67abfdcaa2c6c9c5c1d", size = 83073 }, + { url = "https://files.pythonhosted.org/packages/a0/71/ad95c33da18897e4c636528bbc24a1dd23fe16797de8bc4ec667b8db0ba4/yarl-1.23.0-cp310-cp310-win_amd64.whl", hash = "sha256:ab5f043cb8a2d71c981c09c510da013bc79fd661f5c60139f00dd3c3cc4f2ffb", size = 87328 }, + { url = "https://files.pythonhosted.org/packages/e2/14/dfa369523c79bccf9c9c746b0a63eb31f65db9418ac01275f7950962e504/yarl-1.23.0-cp310-cp310-win_arm64.whl", hash = "sha256:263cd4f47159c09b8b685890af949195b51d1aa82ba451c5847ca9bc6413c220", size = 82463 }, + { url = "https://files.pythonhosted.org/packages/a2/aa/60da938b8f0997ba3a911263c40d82b6f645a67902a490b46f3355e10fae/yarl-1.23.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b35d13d549077713e4414f927cdc388d62e543987c572baee613bf82f11a4b99", size = 123641 }, + { url = "https://files.pythonhosted.org/packages/24/84/e237607faf4e099dbb8a4f511cfd5efcb5f75918baad200ff7380635631b/yarl-1.23.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cbb0fef01f0c6b38cb0f39b1f78fc90b807e0e3c86a7ff3ce74ad77ce5c7880c", size = 86248 }, + { url = "https://files.pythonhosted.org/packages/b2/0d/71ceabc14c146ba8ee3804ca7b3d42b1664c8440439de5214d366fec7d3a/yarl-1.23.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc52310451fc7c629e13c4e061cbe2dd01684d91f2f8ee2821b083c58bd72432", size = 85988 }, + { url = "https://files.pythonhosted.org/packages/8c/6c/4a90d59c572e46b270ca132aca66954f1175abd691f74c1ef4c6711828e2/yarl-1.23.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b2c6b50c7b0464165472b56b42d4c76a7b864597007d9c085e8b63e185cf4a7a", size = 100566 }, + { url = "https://files.pythonhosted.org/packages/49/fb/c438fb5108047e629f6282a371e6e91cf3f97ee087c4fb748a1f32ceef55/yarl-1.23.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:aafe5dcfda86c8af00386d7781d4c2181b5011b7be3f2add5e99899ea925df05", size = 92079 }, + { url = "https://files.pythonhosted.org/packages/d9/13/d269aa1aed3e4f50a5a103f96327210cc5fa5dd2d50882778f13c7a14606/yarl-1.23.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9ee33b875f0b390564c1fb7bc528abf18c8ee6073b201c6ae8524aca778e2d83", size = 108741 }, + { url = "https://files.pythonhosted.org/packages/85/fb/115b16f22c37ea4437d323e472945bea97301c8ec6089868fa560abab590/yarl-1.23.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4c41e021bc6d7affb3364dc1e1e5fa9582b470f283748784bd6ea0558f87f42c", size = 108099 }, + { url = "https://files.pythonhosted.org/packages/9a/64/c53487d9f4968045b8afa51aed7ca44f58b2589e772f32745f3744476c82/yarl-1.23.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:99c8a9ed30f4164bc4c14b37a90208836cbf50d4ce2a57c71d0f52c7fb4f7598", size = 102678 }, + { url = "https://files.pythonhosted.org/packages/85/59/cd98e556fbb2bf8fab29c1a722f67ad45c5f3447cac798ab85620d1e70af/yarl-1.23.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f2af5c81a1f124609d5f33507082fc3f739959d4719b56877ab1ee7e7b3d602b", size = 100803 }, + { url = "https://files.pythonhosted.org/packages/9e/c0/b39770b56d4a9f0bb5f77e2f1763cd2d75cc2f6c0131e3b4c360348fcd65/yarl-1.23.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6b41389c19b07c760c7e427a3462e8ab83c4bb087d127f0e854c706ce1b9215c", size = 100163 }, + { url = "https://files.pythonhosted.org/packages/e7/64/6980f99ab00e1f0ff67cb84766c93d595b067eed07439cfccfc8fb28c1a6/yarl-1.23.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:1dc702e42d0684f42d6519c8d581e49c96cefaaab16691f03566d30658ee8788", size = 93859 }, + { url = "https://files.pythonhosted.org/packages/38/69/912e6c5e146793e5d4b5fe39ff5b00f4d22463dfd5a162bec565ac757673/yarl-1.23.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:0e40111274f340d32ebcc0a5668d54d2b552a6cca84c9475859d364b380e3222", size = 108202 }, + { url = "https://files.pythonhosted.org/packages/59/97/35ca6767524687ad64e5f5c31ad54bc76d585585a9fcb40f649e7e82ffed/yarl-1.23.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:4764a6a7588561a9aef92f65bda2c4fb58fe7c675c0883862e6df97559de0bfb", size = 99866 }, + { url = "https://files.pythonhosted.org/packages/d3/1c/1a3387ee6d73589f6f2a220ae06f2984f6c20b40c734989b0a44f5987308/yarl-1.23.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:03214408cfa590df47728b84c679ae4ef00be2428e11630277be0727eba2d7cc", size = 107852 }, + { url = "https://files.pythonhosted.org/packages/a4/b8/35c0750fcd5a3f781058bfd954515dd4b1eab45e218cbb85cf11132215f1/yarl-1.23.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:170e26584b060879e29fac213e4228ef063f39128723807a312e5c7fec28eff2", size = 102919 }, + { url = "https://files.pythonhosted.org/packages/e5/1c/9a1979aec4a81896d597bcb2177827f2dbee3f5b7cc48b2d0dadb644b41d/yarl-1.23.0-cp311-cp311-win32.whl", hash = "sha256:51430653db848d258336cfa0244427b17d12db63d42603a55f0d4546f50f25b5", size = 82602 }, + { url = "https://files.pythonhosted.org/packages/93/22/b85eca6fa2ad9491af48c973e4c8cf6b103a73dbb271fe3346949449fca0/yarl-1.23.0-cp311-cp311-win_amd64.whl", hash = "sha256:bf49a3ae946a87083ef3a34c8f677ae4243f5b824bfc4c69672e72b3d6719d46", size = 87461 }, + { url = "https://files.pythonhosted.org/packages/93/95/07e3553fe6f113e6864a20bdc53a78113cda3b9ced8784ee52a52c9f80d8/yarl-1.23.0-cp311-cp311-win_arm64.whl", hash = "sha256:b39cb32a6582750b6cc77bfb3c49c0f8760dc18dc96ec9fb55fbb0f04e08b928", size = 82336 }, + { url = "https://files.pythonhosted.org/packages/88/8a/94615bc31022f711add374097ad4144d569e95ff3c38d39215d07ac153a0/yarl-1.23.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1932b6b8bba8d0160a9d1078aae5838a66039e8832d41d2992daa9a3a08f7860", size = 124737 }, + { url = "https://files.pythonhosted.org/packages/e3/6f/c6554045d59d64052698add01226bc867b52fe4a12373415d7991fdca95d/yarl-1.23.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:411225bae281f114067578891bc75534cfb3d92a3b4dfef7a6ca78ba354e6069", size = 87029 }, + { url = "https://files.pythonhosted.org/packages/19/2a/725ecc166d53438bc88f76822ed4b1e3b10756e790bafd7b523fe97c322d/yarl-1.23.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:13a563739ae600a631c36ce096615fe307f131344588b0bc0daec108cdb47b25", size = 86310 }, + { url = "https://files.pythonhosted.org/packages/99/30/58260ed98e6ff7f90ba84442c1ddd758c9170d70327394a6227b310cd60f/yarl-1.23.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cbf44c5cb4a7633d078788e1b56387e3d3cf2b8139a3be38040b22d6c3221c8", size = 97587 }, + { url = "https://files.pythonhosted.org/packages/76/0a/8b08aac08b50682e65759f7f8dde98ae8168f72487e7357a5d684c581ef9/yarl-1.23.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:53ad387048f6f09a8969631e4de3f1bf70c50e93545d64af4f751b2498755072", size = 92528 }, + { url = "https://files.pythonhosted.org/packages/52/07/0b7179101fe5f8385ec6c6bb5d0cb9f76bd9fb4a769591ab6fb5cdbfc69a/yarl-1.23.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4a59ba56f340334766f3a4442e0efd0af895fae9e2b204741ef885c446b3a1a8", size = 105339 }, + { url = "https://files.pythonhosted.org/packages/d3/8a/36d82869ab5ec829ca8574dfcb92b51286fcfb1e9c7a73659616362dc880/yarl-1.23.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:803a3c3ce4acc62eaf01eaca1208dcf0783025ef27572c3336502b9c232005e7", size = 105061 }, + { url = "https://files.pythonhosted.org/packages/66/3e/868e5c3364b6cee19ff3e1a122194fa4ce51def02c61023970442162859e/yarl-1.23.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a3d2bff8f37f8d0f96c7ec554d16945050d54462d6e95414babaa18bfafc7f51", size = 100132 }, + { url = "https://files.pythonhosted.org/packages/cf/26/9c89acf82f08a52cb52d6d39454f8d18af15f9d386a23795389d1d423823/yarl-1.23.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c75eb09e8d55bceb4367e83496ff8ef2bc7ea6960efb38e978e8073ea59ecb67", size = 99289 }, + { url = "https://files.pythonhosted.org/packages/6f/54/5b0db00d2cb056922356104468019c0a132e89c8d3ab67d8ede9f4483d2a/yarl-1.23.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877b0738624280e34c55680d6054a307aa94f7d52fa0e3034a9cc6e790871da7", size = 96950 }, + { url = "https://files.pythonhosted.org/packages/f6/40/10fa93811fd439341fad7e0718a86aca0de9548023bbb403668d6555acab/yarl-1.23.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b5405bb8f0e783a988172993cfc627e4d9d00432d6bbac65a923041edacf997d", size = 93960 }, + { url = "https://files.pythonhosted.org/packages/bc/d2/8ae2e6cd77d0805f4526e30ec43b6f9a3dfc542d401ac4990d178e4bf0cf/yarl-1.23.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1c3a3598a832590c5a3ce56ab5576361b5688c12cb1d39429cf5dba30b510760", size = 104703 }, + { url = "https://files.pythonhosted.org/packages/2f/0c/b3ceacf82c3fe21183ce35fa2acf5320af003d52bc1fcf5915077681142e/yarl-1.23.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:8419ebd326430d1cbb7efb5292330a2cf39114e82df5cc3d83c9a0d5ebeaf2f2", size = 98325 }, + { url = "https://files.pythonhosted.org/packages/9d/e0/12900edd28bdab91a69bd2554b85ad7b151f64e8b521fe16f9ad2f56477a/yarl-1.23.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:be61f6fff406ca40e3b1d84716fde398fc08bc63dd96d15f3a14230a0973ed86", size = 105067 }, + { url = "https://files.pythonhosted.org/packages/15/61/74bb1182cf79c9bbe4eb6b1f14a57a22d7a0be5e9cedf8e2d5c2086474c3/yarl-1.23.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3ceb13c5c858d01321b5d9bb65e4cf37a92169ea470b70fec6f236b2c9dd7e34", size = 100285 }, + { url = "https://files.pythonhosted.org/packages/69/7f/cd5ef733f2550de6241bd8bd8c3febc78158b9d75f197d9c7baa113436af/yarl-1.23.0-cp312-cp312-win32.whl", hash = "sha256:fffc45637bcd6538de8b85f51e3df3223e4ad89bccbfca0481c08c7fc8b7ed7d", size = 82359 }, + { url = "https://files.pythonhosted.org/packages/f5/be/25216a49daeeb7af2bec0db22d5e7df08ed1d7c9f65d78b14f3b74fd72fc/yarl-1.23.0-cp312-cp312-win_amd64.whl", hash = "sha256:f69f57305656a4852f2a7203efc661d8c042e6cc67f7acd97d8667fb448a426e", size = 87674 }, + { url = "https://files.pythonhosted.org/packages/d2/35/aeab955d6c425b227d5b7247eafb24f2653fedc32f95373a001af5dfeb9e/yarl-1.23.0-cp312-cp312-win_arm64.whl", hash = "sha256:6e87a6e8735b44816e7db0b2fbc9686932df473c826b0d9743148432e10bb9b9", size = 81879 }, + { url = "https://files.pythonhosted.org/packages/9a/4b/a0a6e5d0ee8a2f3a373ddef8a4097d74ac901ac363eea1440464ccbe0898/yarl-1.23.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:16c6994ac35c3e74fb0ae93323bf8b9c2a9088d55946109489667c510a7d010e", size = 123796 }, + { url = "https://files.pythonhosted.org/packages/67/b6/8925d68af039b835ae876db5838e82e76ec87b9782ecc97e192b809c4831/yarl-1.23.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4a42e651629dafb64fd5b0286a3580613702b5809ad3f24934ea87595804f2c5", size = 86547 }, + { url = "https://files.pythonhosted.org/packages/ae/50/06d511cc4b8e0360d3c94af051a768e84b755c5eb031b12adaaab6dec6e5/yarl-1.23.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7c6b9461a2a8b47c65eef63bb1c76a4f1c119618ffa99ea79bc5bb1e46c5821b", size = 85854 }, + { url = "https://files.pythonhosted.org/packages/c4/f4/4e30b250927ffdab4db70da08b9b8d2194d7c7b400167b8fbeca1e4701ca/yarl-1.23.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2569b67d616eab450d262ca7cb9f9e19d2f718c70a8b88712859359d0ab17035", size = 98351 }, + { url = "https://files.pythonhosted.org/packages/86/fc/4118c5671ea948208bdb1492d8b76bdf1453d3e73df051f939f563e7dcc5/yarl-1.23.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e9d9a4d06d3481eab79803beb4d9bd6f6a8e781ec078ac70d7ef2dcc29d1bea5", size = 92711 }, + { url = "https://files.pythonhosted.org/packages/56/11/1ed91d42bd9e73c13dc9e7eb0dd92298d75e7ac4dd7f046ad0c472e231cd/yarl-1.23.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f514f6474e04179d3d33175ed3f3e31434d3130d42ec153540d5b157deefd735", size = 106014 }, + { url = "https://files.pythonhosted.org/packages/ce/c9/74e44e056a23fbc33aca71779ef450ca648a5bc472bdad7a82339918f818/yarl-1.23.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fda207c815b253e34f7e1909840fd14299567b1c0eb4908f8c2ce01a41265401", size = 105557 }, + { url = "https://files.pythonhosted.org/packages/66/fe/b1e10b08d287f518994f1e2ff9b6d26f0adeecd8dd7d533b01bab29a3eda/yarl-1.23.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34b6cf500e61c90f305094911f9acc9c86da1a05a7a3f5be9f68817043f486e4", size = 101559 }, + { url = "https://files.pythonhosted.org/packages/72/59/c5b8d94b14e3d3c2a9c20cb100119fd534ab5a14b93673ab4cc4a4141ea5/yarl-1.23.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d7504f2b476d21653e4d143f44a175f7f751cd41233525312696c76aa3dbb23f", size = 100502 }, + { url = "https://files.pythonhosted.org/packages/77/4f/96976cb54cbfc5c9fd73ed4c51804f92f209481d1fb190981c0f8a07a1d7/yarl-1.23.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:578110dd426f0d209d1509244e6d4a3f1a3e9077655d98c5f22583d63252a08a", size = 98027 }, + { url = "https://files.pythonhosted.org/packages/63/6e/904c4f476471afdbad6b7e5b70362fb5810e35cd7466529a97322b6f5556/yarl-1.23.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:609d3614d78d74ebe35f54953c5bbd2ac647a7ddb9c30a5d877580f5e86b22f2", size = 95369 }, + { url = "https://files.pythonhosted.org/packages/9d/40/acfcdb3b5f9d68ef499e39e04d25e141fe90661f9d54114556cf83be8353/yarl-1.23.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4966242ec68afc74c122f8459abd597afd7d8a60dc93d695c1334c5fd25f762f", size = 105565 }, + { url = "https://files.pythonhosted.org/packages/5e/c6/31e28f3a6ba2869c43d124f37ea5260cac9c9281df803c354b31f4dd1f3c/yarl-1.23.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e0fd068364a6759bc794459f0a735ab151d11304346332489c7972bacbe9e72b", size = 99813 }, + { url = "https://files.pythonhosted.org/packages/08/1f/6f65f59e72d54aa467119b63fc0b0b1762eff0232db1f4720cd89e2f4a17/yarl-1.23.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:39004f0ad156da43e86aa71f44e033de68a44e5a31fc53507b36dd253970054a", size = 105632 }, + { url = "https://files.pythonhosted.org/packages/a3/c4/18b178a69935f9e7a338127d5b77d868fdc0f0e49becd286d51b3a18c61d/yarl-1.23.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e5723c01a56c5028c807c701aa66722916d2747ad737a046853f6c46f4875543", size = 101895 }, + { url = "https://files.pythonhosted.org/packages/8f/54/f5b870b5505663911dba950a8e4776a0dbd51c9c54c0ae88e823e4b874a0/yarl-1.23.0-cp313-cp313-win32.whl", hash = "sha256:1b6b572edd95b4fa8df75de10b04bc81acc87c1c7d16bcdd2035b09d30acc957", size = 82356 }, + { url = "https://files.pythonhosted.org/packages/7a/84/266e8da36879c6edcd37b02b547e2d9ecdfea776be49598e75696e3316e1/yarl-1.23.0-cp313-cp313-win_amd64.whl", hash = "sha256:baaf55442359053c7d62f6f8413a62adba3205119bcb6f49594894d8be47e5e3", size = 87515 }, + { url = "https://files.pythonhosted.org/packages/00/fd/7e1c66efad35e1649114fa13f17485f62881ad58edeeb7f49f8c5e748bf9/yarl-1.23.0-cp313-cp313-win_arm64.whl", hash = "sha256:fb4948814a2a98e3912505f09c9e7493b1506226afb1f881825368d6fb776ee3", size = 81785 }, + { url = "https://files.pythonhosted.org/packages/9c/fc/119dd07004f17ea43bb91e3ece6587759edd7519d6b086d16bfbd3319982/yarl-1.23.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:aecfed0b41aa72b7881712c65cf764e39ce2ec352324f5e0837c7048d9e6daaa", size = 130719 }, + { url = "https://files.pythonhosted.org/packages/e6/0d/9f2348502fbb3af409e8f47730282cd6bc80dec6630c1e06374d882d6eb2/yarl-1.23.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a41bcf68efd19073376eb8cf948b8d9be0af26256403e512bb18f3966f1f9120", size = 89690 }, + { url = "https://files.pythonhosted.org/packages/50/93/e88f3c80971b42cfc83f50a51b9d165a1dbf154b97005f2994a79f212a07/yarl-1.23.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cde9a2ecd91668bcb7f077c4966d8ceddb60af01b52e6e3e2680e4cf00ad1a59", size = 89851 }, + { url = "https://files.pythonhosted.org/packages/1c/07/61c9dd8ba8f86473263b4036f70fb594c09e99c0d9737a799dfd8bc85651/yarl-1.23.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5023346c4ee7992febc0068e7593de5fa2bf611848c08404b35ebbb76b1b0512", size = 95874 }, + { url = "https://files.pythonhosted.org/packages/9e/e9/f9ff8ceefba599eac6abddcfb0b3bee9b9e636e96dbf54342a8577252379/yarl-1.23.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d1009abedb49ae95b136a8904a3f71b342f849ffeced2d3747bf29caeda218c4", size = 88710 }, + { url = "https://files.pythonhosted.org/packages/eb/78/0231bfcc5d4c8eec220bc2f9ef82cb4566192ea867a7c5b4148f44f6cbcd/yarl-1.23.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a8d00f29b42f534cc8aa3931cfe773b13b23e561e10d2b26f27a8d309b0e82a1", size = 101033 }, + { url = "https://files.pythonhosted.org/packages/cd/9b/30ea5239a61786f18fd25797151a17fbb3be176977187a48d541b5447dd4/yarl-1.23.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:95451e6ce06c3e104556d73b559f5da6c34a069b6b62946d3ad66afcd51642ea", size = 100817 }, + { url = "https://files.pythonhosted.org/packages/62/e2/a4980481071791bc83bce2b7a1a1f7adcabfa366007518b4b845e92eeee3/yarl-1.23.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:531ef597132086b6cf96faa7c6c1dcd0361dd5f1694e5cc30375907b9b7d3ea9", size = 97482 }, + { url = "https://files.pythonhosted.org/packages/e5/1e/304a00cf5f6100414c4b5a01fc7ff9ee724b62158a08df2f8170dfc72a2d/yarl-1.23.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:88f9fb0116fbfcefcab70f85cf4b74a2b6ce5d199c41345296f49d974ddb4123", size = 95949 }, + { url = "https://files.pythonhosted.org/packages/68/03/093f4055ed4cae649ac53bca3d180bd37102e9e11d048588e9ab0c0108d0/yarl-1.23.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e7b0460976dc75cb87ad9cc1f9899a4b97751e7d4e77ab840fc9b6d377b8fd24", size = 95839 }, + { url = "https://files.pythonhosted.org/packages/b9/28/4c75ebb108f322aa8f917ae10a8ffa4f07cae10a8a627b64e578617df6a0/yarl-1.23.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:115136c4a426f9da976187d238e84139ff6b51a20839aa6e3720cd1026d768de", size = 90696 }, + { url = "https://files.pythonhosted.org/packages/23/9c/42c2e2dd91c1a570402f51bdf066bfdb1241c2240ba001967bad778e77b7/yarl-1.23.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:ead11956716a940c1abc816b7df3fa2b84d06eaed8832ca32f5c5e058c65506b", size = 100865 }, + { url = "https://files.pythonhosted.org/packages/74/05/1bcd60a8a0a914d462c305137246b6f9d167628d73568505fce3f1cb2e65/yarl-1.23.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:fe8f8f5e70e6dbdfca9882cd9deaac058729bcf323cf7a58660901e55c9c94f6", size = 96234 }, + { url = "https://files.pythonhosted.org/packages/90/b2/f52381aac396d6778ce516b7bc149c79e65bfc068b5de2857ab69eeea3b7/yarl-1.23.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:a0e317df055958a0c1e79e5d2aa5a5eaa4a6d05a20d4b0c9c3f48918139c9fc6", size = 100295 }, + { url = "https://files.pythonhosted.org/packages/e5/e8/638bae5bbf1113a659b2435d8895474598afe38b4a837103764f603aba56/yarl-1.23.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f0fd84de0c957b2d280143522c4f91a73aada1923caee763e24a2b3fda9f8a5", size = 97784 }, + { url = "https://files.pythonhosted.org/packages/80/25/a3892b46182c586c202629fc2159aa13975d3741d52ebd7347fd501d48d5/yarl-1.23.0-cp313-cp313t-win32.whl", hash = "sha256:93a784271881035ab4406a172edb0faecb6e7d00f4b53dc2f55919d6c9688595", size = 88313 }, + { url = "https://files.pythonhosted.org/packages/43/68/8c5b36aa5178900b37387937bc2c2fe0e9505537f713495472dcf6f6fccc/yarl-1.23.0-cp313-cp313t-win_amd64.whl", hash = "sha256:dd00607bffbf30250fe108065f07453ec124dbf223420f57f5e749b04295e090", size = 94932 }, + { url = "https://files.pythonhosted.org/packages/c6/cc/d79ba8292f51f81f4dc533a8ccfb9fc6992cabf0998ed3245de7589dc07c/yarl-1.23.0-cp313-cp313t-win_arm64.whl", hash = "sha256:ac09d42f48f80c9ee1635b2fcaa819496a44502737660d3c0f2ade7526d29144", size = 84786 }, + { url = "https://files.pythonhosted.org/packages/90/98/b85a038d65d1b92c3903ab89444f48d3cee490a883477b716d7a24b1a78c/yarl-1.23.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:21d1b7305a71a15b4794b5ff22e8eef96ff4a6d7f9657155e5aa419444b28912", size = 124455 }, + { url = "https://files.pythonhosted.org/packages/39/54/bc2b45559f86543d163b6e294417a107bb87557609007c007ad889afec18/yarl-1.23.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:85610b4f27f69984932a7abbe52703688de3724d9f72bceb1cca667deff27474", size = 86752 }, + { url = "https://files.pythonhosted.org/packages/24/f9/e8242b68362bffe6fb536c8db5076861466fc780f0f1b479fc4ffbebb128/yarl-1.23.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23f371bd662cf44a7630d4d113101eafc0cfa7518a2760d20760b26021454719", size = 86291 }, + { url = "https://files.pythonhosted.org/packages/ea/d8/d1cb2378c81dd729e98c716582b1ccb08357e8488e4c24714658cc6630e8/yarl-1.23.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4a80f77dc1acaaa61f0934176fccca7096d9b1ff08c8ba9cddf5ae034a24319", size = 99026 }, + { url = "https://files.pythonhosted.org/packages/0a/ff/7196790538f31debe3341283b5b0707e7feb947620fc5e8236ef28d44f72/yarl-1.23.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:bd654fad46d8d9e823afbb4f87c79160b5a374ed1ff5bde24e542e6ba8f41434", size = 92355 }, + { url = "https://files.pythonhosted.org/packages/c1/56/25d58c3eddde825890a5fe6aa1866228377354a3c39262235234ab5f616b/yarl-1.23.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:682bae25f0a0dd23a056739f23a134db9f52a63e2afd6bfb37ddc76292bbd723", size = 106417 }, + { url = "https://files.pythonhosted.org/packages/51/8a/882c0e7bc8277eb895b31bce0138f51a1ba551fc2e1ec6753ffc1e7c1377/yarl-1.23.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a82836cab5f197a0514235aaf7ffccdc886ccdaa2324bc0aafdd4ae898103039", size = 106422 }, + { url = "https://files.pythonhosted.org/packages/42/2b/fef67d616931055bf3d6764885990a3ac647d68734a2d6a9e1d13de437a2/yarl-1.23.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c57676bdedc94cd3bc37724cf6f8cd2779f02f6aba48de45feca073e714fe52", size = 101915 }, + { url = "https://files.pythonhosted.org/packages/18/6a/530e16aebce27c5937920f3431c628a29a4b6b430fab3fd1c117b26ff3f6/yarl-1.23.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c7f8dc16c498ff06497c015642333219871effba93e4a2e8604a06264aca5c5c", size = 100690 }, + { url = "https://files.pythonhosted.org/packages/88/08/93749219179a45e27b036e03260fda05190b911de8e18225c294ac95bbc9/yarl-1.23.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:5ee586fb17ff8f90c91cf73c6108a434b02d69925f44f5f8e0d7f2f260607eae", size = 98750 }, + { url = "https://files.pythonhosted.org/packages/d9/cf/ea424a004969f5d81a362110a6ac1496d79efdc6d50c2c4b2e3ea0fc2519/yarl-1.23.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:17235362f580149742739cc3828b80e24029d08cbb9c4bda0242c7b5bc610a8e", size = 94685 }, + { url = "https://files.pythonhosted.org/packages/e2/b7/14341481fe568e2b0408bcf1484c652accafe06a0ade9387b5d3fd9df446/yarl-1.23.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:0793e2bd0cf14234983bbb371591e6bea9e876ddf6896cdcc93450996b0b5c85", size = 106009 }, + { url = "https://files.pythonhosted.org/packages/0a/e6/5c744a9b54f4e8007ad35bce96fbc9218338e84812d36f3390cea616881a/yarl-1.23.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:3650dc2480f94f7116c364096bc84b1d602f44224ef7d5c7208425915c0475dd", size = 100033 }, + { url = "https://files.pythonhosted.org/packages/0c/23/e3bfc188d0b400f025bc49d99793d02c9abe15752138dcc27e4eaf0c4a9e/yarl-1.23.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f40e782d49630ad384db66d4d8b73ff4f1b8955dc12e26b09a3e3af064b3b9d6", size = 106483 }, + { url = "https://files.pythonhosted.org/packages/72/42/f0505f949a90b3f8b7a363d6cbdf398f6e6c58946d85c6d3a3bc70595b26/yarl-1.23.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:94f8575fbdf81749008d980c17796097e645574a3b8c28ee313931068dad14fe", size = 102175 }, + { url = "https://files.pythonhosted.org/packages/aa/65/b39290f1d892a9dd671d1c722014ca062a9c35d60885d57e5375db0404b5/yarl-1.23.0-cp314-cp314-win32.whl", hash = "sha256:c8aa34a5c864db1087d911a0b902d60d203ea3607d91f615acd3f3108ac32169", size = 83871 }, + { url = "https://files.pythonhosted.org/packages/a9/5b/9b92f54c784c26e2a422e55a8d2607ab15b7ea3349e28359282f84f01d43/yarl-1.23.0-cp314-cp314-win_amd64.whl", hash = "sha256:63e92247f383c85ab00dd0091e8c3fa331a96e865459f5ee80353c70a4a42d70", size = 89093 }, + { url = "https://files.pythonhosted.org/packages/e0/7d/8a84dc9381fd4412d5e7ff04926f9865f6372b4c2fd91e10092e65d29eb8/yarl-1.23.0-cp314-cp314-win_arm64.whl", hash = "sha256:70efd20be968c76ece7baa8dafe04c5be06abc57f754d6f36f3741f7aa7a208e", size = 83384 }, + { url = "https://files.pythonhosted.org/packages/dd/8d/d2fad34b1c08aa161b74394183daa7d800141aaaee207317e82c790b418d/yarl-1.23.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:9a18d6f9359e45722c064c97464ec883eb0e0366d33eda61cb19a244bf222679", size = 131019 }, + { url = "https://files.pythonhosted.org/packages/19/ff/33009a39d3ccf4b94d7d7880dfe17fb5816c5a4fe0096d9b56abceea9ac7/yarl-1.23.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:2803ed8b21ca47a43da80a6fd1ed3019d30061f7061daa35ac54f63933409412", size = 89894 }, + { url = "https://files.pythonhosted.org/packages/0c/f1/dab7ac5e7306fb79c0190766a3c00b4cb8d09a1f390ded68c85a5934faf5/yarl-1.23.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:394906945aa8b19fc14a61cf69743a868bb8c465efe85eee687109cc540b98f4", size = 89979 }, + { url = "https://files.pythonhosted.org/packages/aa/b1/08e95f3caee1fad6e65017b9f26c1d79877b502622d60e517de01e72f95d/yarl-1.23.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:71d006bee8397a4a89f469b8deb22469fe7508132d3c17fa6ed871e79832691c", size = 95943 }, + { url = "https://files.pythonhosted.org/packages/c0/cc/6409f9018864a6aa186c61175b977131f373f1988e198e031236916e87e4/yarl-1.23.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:62694e275c93d54f7ccedcfef57d42761b2aad5234b6be1f3e3026cae4001cd4", size = 88786 }, + { url = "https://files.pythonhosted.org/packages/76/40/cc22d1d7714b717fde2006fad2ced5efe5580606cb059ae42117542122f3/yarl-1.23.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a31de1613658308efdb21ada98cbc86a97c181aa050ba22a808120bb5be3ab94", size = 101307 }, + { url = "https://files.pythonhosted.org/packages/8f/0d/476c38e85ddb4c6ec6b20b815bdd779aa386a013f3d8b85516feee55c8dc/yarl-1.23.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fb1e8b8d66c278b21d13b0a7ca22c41dd757a7c209c6b12c313e445c31dd3b28", size = 100904 }, + { url = "https://files.pythonhosted.org/packages/72/32/0abe4a76d59adf2081dcb0397168553ece4616ada1c54d1c49d8936c74f8/yarl-1.23.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50f9d8d531dfb767c565f348f33dd5139a6c43f5cbdf3f67da40d54241df93f6", size = 97728 }, + { url = "https://files.pythonhosted.org/packages/b7/35/7b30f4810fba112f60f5a43237545867504e15b1c7647a785fbaf588fac2/yarl-1.23.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:575aa4405a656e61a540f4a80eaa5260f2a38fff7bfdc4b5f611840d76e9e277", size = 95964 }, + { url = "https://files.pythonhosted.org/packages/2d/86/ed7a73ab85ef00e8bb70b0cb5421d8a2a625b81a333941a469a6f4022828/yarl-1.23.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:041b1a4cefacf65840b4e295c6985f334ba83c30607441ae3cf206a0eed1a2e4", size = 95882 }, + { url = "https://files.pythonhosted.org/packages/19/90/d56967f61a29d8498efb7afb651e0b2b422a1e9b47b0ab5f4e40a19b699b/yarl-1.23.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:d38c1e8231722c4ce40d7593f28d92b5fc72f3e9774fe73d7e800ec32299f63a", size = 90797 }, + { url = "https://files.pythonhosted.org/packages/72/00/8b8f76909259f56647adb1011d7ed8b321bcf97e464515c65016a47ecdf0/yarl-1.23.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:d53834e23c015ee83a99377db6e5e37d8484f333edb03bd15b4bc312cc7254fb", size = 101023 }, + { url = "https://files.pythonhosted.org/packages/ac/e2/cab11b126fb7d440281b7df8e9ddbe4851e70a4dde47a202b6642586b8d9/yarl-1.23.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:2e27c8841126e017dd2a054a95771569e6070b9ee1b133366d8b31beb5018a41", size = 96227 }, + { url = "https://files.pythonhosted.org/packages/c2/9b/2c893e16bfc50e6b2edf76c1a9eb6cb0c744346197e74c65e99ad8d634d0/yarl-1.23.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:76855800ac56f878847a09ce6dba727c93ca2d89c9e9d63002d26b916810b0a2", size = 100302 }, + { url = "https://files.pythonhosted.org/packages/28/ec/5498c4e3a6d5f1003beb23405671c2eb9cdbf3067d1c80f15eeafe301010/yarl-1.23.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e09fd068c2e169a7070d83d3bde728a4d48de0549f975290be3c108c02e499b4", size = 98202 }, + { url = "https://files.pythonhosted.org/packages/fe/c3/cd737e2d45e70717907f83e146f6949f20cc23cd4bf7b2688727763aa458/yarl-1.23.0-cp314-cp314t-win32.whl", hash = "sha256:73309162a6a571d4cbd3b6a1dcc703c7311843ae0d1578df6f09be4e98df38d4", size = 90558 }, + { url = "https://files.pythonhosted.org/packages/e1/19/3774d162f6732d1cfb0b47b4140a942a35ca82bb19b6db1f80e9e7bdc8f8/yarl-1.23.0-cp314-cp314t-win_amd64.whl", hash = "sha256:4503053d296bc6e4cbd1fad61cf3b6e33b939886c4f249ba7c78b602214fabe2", size = 97610 }, + { url = "https://files.pythonhosted.org/packages/51/47/3fa2286c3cb162c71cdb34c4224d5745a1ceceb391b2bd9b19b668a8d724/yarl-1.23.0-cp314-cp314t-win_arm64.whl", hash = "sha256:44bb7bef4ea409384e3f8bc36c063d77ea1b8d4a5b2706956c0d6695f07dcc25", size = 86041 }, + { url = "https://files.pythonhosted.org/packages/69/68/c8739671f5699c7dc470580a4f821ef37c32c4cb0b047ce223a7f115757f/yarl-1.23.0-py3-none-any.whl", hash = "sha256:a2df6afe50dea8ae15fa34c9f824a3ee958d785fd5d089063d960bae1daa0a3f", size = 48288 }, +]